简体   繁体   中英

How to "widen" reader type when using fp-ts sequenceT?

I'm wondering if it's possible to "widen" my ultimate Reader type when using sequenceT? This is possible when chaining operations sequentially using chainW etc., but it looks like when using sequenceT you're stuck with every item having to use the same Reader type. I'd like to do this so I'm able to parallelise execution of some tasks where appropriate, but still be able to use dependency injection via Reader.

Example:

import { sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { pipe } from 'fp-ts/lib/function'
import * as RT from 'fp-ts/ReaderTask'

interface Person {
  name: string
}

const getMe = (name: string) => (deps: { api: any }) => async (): Promise<Person> => {
  const person = {
    name,
  }
  return person
}

const getMum = (child: Person) => (deps: { api2: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s mum",
  }
  return person
}

const getDad = (child: Person) => (deps: { api2: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s dad",
  }
  return person
}

const getFamily = (name: string) => 
  pipe(
    getMe(name),
    RT.chainW(me => 
      sequenceT(RT.readerTask)(
        getMum(me), 
        getDad(me))
    ))

getFamily('John')({ api: 'x', api2: 'y' })().then(
  ([mum, dad]) => {
    log(mum)()
    log(dad)()
  })

This compiles fine and outputs:

$ node dist/src/index.js
{ name: "John's mum" }
{ name: "John's dad" }

Now let's say getDad relies on a different api, say api3. If I update the code it no longer compiles, because getMum and getDad aren't using the same Reader type.

Example (does not compile):

import { sequenceT } from 'fp-ts/lib/Apply'
import { log } from 'fp-ts/lib/Console'
import { pipe } from 'fp-ts/lib/function'
import * as RT from 'fp-ts/ReaderTask'

interface Person {
  name: string
}

const getMe = (name: string) => (deps: { api: any }) => async (): Promise<Person> => {
  const person = {
    name,
  }
  return person
}

const getMum = (child: Person) => (deps: { api2: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s mum",
  }
  return person
}

const getDad = (child: Person) => (deps: { api3: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s dad",
  }
  return person
}

const getFamily = (name: string) => 
  pipe(
    getMe(name),
    RT.chainW(me => 
      sequenceT(RT.readerTask)(
        getMum(me), // compiler complains here
        getDad(me))
    ))

getFamily('John')({ api: 'x', api2: 'y', api3: 'z' })().then( // compiler complains here, on api3
  ([mum, dad]) => {
    log(mum)()
    log(dad)()
  })

I was actually trying this with StateReaderTaskEither but simplified it to use ReaderTask for this example - sequenceT exhibits the same restriction with that too however.

Any ideas how to solve?

This is exactly what Reader/ReaderTask/ReaderTaskEither.local is for! I use this regularly. For example, if you are parallelizing HTTP calls to an API where some require an auth token + base URL, while others only require base URL (so some use interface Auth { token:string, baseUrl: string } while others use interface NoAuth { baseUrl: string } .

  interface Apis {
    java: JavaRepository,
    db: DbRepository,
    redis: RedisRepository,
  }
  interface DomainError {}

  declare const javaApiCall: RTE<JavaRepository, DomainError, JavaResult>
  declare const dbApiCall: RTE<DbRepository, DomainError, DbResult>
  declare const redisApiCall: RTE<RedisRepository, DomainError, RedisResult>
  declare const apis: Apis

  const getJava = (apis:Apis) => apis.java
  const getDb = (apis:Apis) => apis.db
  const getRedis = (apis:Apis) => apis.redis

  sequenceT(readerTaskEither)(
    RTE.local(getJava)(javaApiCall),
    RTE.local(getDb)(dbApiCall),
    RTE.local(getRedix)(redisApiCall),
  )(apis) // TaskEither<DomainError, [JavaResult,DbResult,RedisResult]>

I figured this one out after a little more reading of fp-ts code. The answer I came up with is to just do what sequenceT effectively does manually.

Here's my solution:

import { log } from 'fp-ts/lib/Console'
import { pipe } from 'fp-ts/lib/function'
import * as RT from 'fp-ts/ReaderTask'

interface Person {
  name: string
}

const getMe = (name: string) => (deps: { api: any }) => async (): Promise<Person> => {
  const person = {
    name,
  }
  return person
}

const getMum = (child: Person) => (deps: { api2: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s mum",
  }
  return person
}

const getDad = (child: Person) => (deps: { api3: any }) => async (): Promise<Person> => {
  const person = {
    name: child.name + "'s dad",
  }
  return person
}

const getFamily = (name: string) => 
  pipe(
    getMe(name),
    RT.chainW(me => 
      pipe(
        RT.of((mum: Person) => (dad: Person) => [mum, dad]),
        RT.apW(getMum(me)),
        RT.apW(getDad(me))
      )
    ))

getFamily('John')({ api: 'x', api2: 'y', api3: 'z' })().then(
  ([mum, dad]) => {
    log(mum)()
    log(dad)()
  })

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM