4

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?

Jess
  • 387
  • 1
  • 4
  • 9

2 Answers2

6

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]>
user1713450
  • 1,307
  • 7
  • 18
  • 1
    Even better! Thanks for highlighting. Fp-ts is great but it’s a bit of a treasure hunt with the big lack of practical documentation. – Jess Dec 06 '20 at 00:25
  • `fp-ts` certainly assumes a level of familiarity with FP people might not have when they first come to it. I was lucky I had cut my teeth in FP using Arrow in Kotlin, so I was familiar with things like a reader's `local` and what it's for. I remember when I was first using it in Kotlin and not knowing what in the world that was for until one day I saw it used. – user1713450 Dec 17 '20 at 07:34
0

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)()
  })
Jess
  • 387
  • 1
  • 4
  • 9