loading...

Getting started with fp-ts: Reader

gcanti profile image Giulio Canti ・3 min read

The purpose of the Reader monad is to avoid threading arguments through multiple functions in order to only get them where they are needed.

One of the ideas presented here is to use the Reader monad for dependency injection.

The first thing you need to know is that the type Reader<R, A> represents a function (r: R) => A

interface Reader<R, A> {
  (r: R): A
}

where R represents an "environment" needed for the computation (we can "read" from it) and A is the result.

Example

Let's say we have the following piece of code

const f = (b: boolean): string => (b ? 'true' : 'false')

const g = (n: number): string => f(n > 2)

const h = (s: string): string => g(s.length + 1)

console.log(h('foo')) // 'true'

What if we want to internationalise f? Well, we could add an additional parameter

interface Dependencies {
  i18n: {
    true: string
    false: string
  }
}

const f = (b: boolean, deps: Dependencies): string => (b ? deps.i18n.true : deps.i18n.false)

Now we have a problem though, g doesn't compile anymore

const g = (n: number): string => f(n > 2) // error: An argument for 'deps' was not provided

We must add an additional parameter to g as well

const g = (n: number, deps: Dependencies): string => f(n > 2, deps) // ok

We haven't finished yet, now it's h that doesn't compile, we must add an additional parameter to h as well

const h = (s: string, deps: Dependencies): string => g(s.length + 1, deps)

finally we can run h by providing an actual instance of the Dependencies interface

const instance: Dependencies = {
  i18n: {
    true: 'vero',
    false: 'falso'
  }
}

console.log(h('foo', instance)) // 'vero'

As you can see, h and g must have knowledge about f dependencies despite not using them.

Can we improve this part? Yes we can, we can move Dependencies from the parameter list to the return type.

Reader

Let's start by rewriting our functions, putting the deps parameter alone

const f = (b: boolean): ((deps: Dependencies) => string) => deps => (b ? deps.i18n.true : deps.i18n.false)

const g = (n: number): ((deps: Dependencies) => string) => f(n > 2)

const h = (s: string): ((deps: Dependencies) => string) => g(s.length + 1)

Note that (deps: Dependencies) => string is just Reader<Dependencies, string>

import { Reader } from 'fp-ts/lib/Reader'

const f = (b: boolean): Reader<Dependencies, string> => deps => (b ? deps.i18n.true : deps.i18n.false)

const g = (n: number): Reader<Dependencies, string> => f(n > 2)

const h = (s: string): Reader<Dependencies, string> => g(s.length + 1)

console.log(h('foo')(instance)) // 'vero'

ask

What if we want to also inject the lower bound (2 in our example) in g? Let's add a new field to Dependencies first

export interface Dependencies {
  i18n: {
    true: string
    false: string
  }
  lowerBound: number
}

const instance: Dependencies = {
  i18n: {
    true: 'vero',
    false: 'falso'
  },
  lowerBound: 2
}

Now we can read lowerBound from the environment using ask

import { pipe } from 'fp-ts/lib/pipeable'
import { ask, chain, Reader } from 'fp-ts/lib/Reader'

const g = (n: number): Reader<Dependencies, string> =>
  pipe(
    ask<Dependencies>(),
    chain(deps => f(n > deps.lowerBound))
  )

console.log(h('foo')(instance)) // 'vero'
console.log(h('foo')({ ...instance, lowerBound: 4 })) // 'falso'

p.s.

As a curiosity, Reader's map is (the usual) function composition

import { flow } from 'fp-ts/lib/function'
import { pipe } from 'fp-ts/lib/pipeable'
import { map } from 'fp-ts/lib/Reader'

const len = (s: string): number => s.length
const double = (n: number): number => n * 2
const gt2 = (n: number): boolean => n > 2

const composition = flow(len, double, gt2)
// equivalent to
const composition = pipe(len, map(double), map(gt2))

Posted on by:

Discussion

markdown guide
 

Very nice functional concept. But I have small concern if the whole typing of that has/or not a big sense. I mean we really use here a HOF and the whole composition h . g . f result is a function with deps argument

const f = (b: boolean) => (deps: Dependencies) => (b ? deps.i18n.true : deps.i18n.false)
const g = (n: number) => f(n > 2)
const h = (s: string) => g(s.length + 1)

For the second, I see some benefit, but not see doing it like below as a bad thing (yes deps are passed explicitly, but is it any issue?)

const f = (b: boolean) => (deps: Dependencies) => (b ? deps.i18n.true : deps.i18n.false)
const g = (n: number) => (deps: Dependencies) => f(n > deps.lowerBound)(deps) 
// above explicit deps which we can use
const h = (s: string) => g(s.length + 1)

Not get me wrong, just thinking if the whole additional piping and typing is beneficial here.

 

I have small concern if the whole typing of that has/or not a big sense

Well, you are just writing a bunch of functions returning Reader, whether or not you add an explicit type. The point is making the dependencies as implicit as possibile, the key is to put them as a last argument. Note that often people put the dependencies as a first argument

const f = (deps: Dependencies) => (b: boolean) => (b ? deps.i18n.true : deps.i18n.false)

making composition more difficult.

For the second...

all the following gs are equivalent (so feel free to choose the style you prefer)

const g = (n: number) => (deps: Dependencies) => pipe(deps, f(n > deps.lowerBound))

const g = (n: number): Reader<Dependencies, string> => deps => pipe(deps, f(n > deps.lowerBound))

const g = (n: number): Reader<Dependencies, string> => deps => f(n > deps.lowerBound)(deps)

const g = (n: number): Reader<Dependencies, string> =>
  pipe(
    ask<Dependencies>(),
    chain(deps => f(n > deps.lowerBound))
  )
 

I'm used to put deps as the first argument since this lets you partially apply f:

const f = (deps: Dependencies) => (b: boolean): string => (...)

 

Yes the whole idea has a lot of sense, and I mean adding argument after not before. Thanks for clarifying!

 

I'd say there'e no magic in here - in FP all functions have to be referentially transparent at the end of the day i.e. all arguments have to be passed directly! What I think is crucial in here is the conceptual "turning inside-out" (or rather "bottom-up", should I say) - instead of passing props/arguments down multiple levels, we pass a function consuming these props (Reader) multiple levels up, in an almost implicit way. When the Reader reaches "the surface", we can pass its dependencies directly.

 

Yes I see this in that way also. The whole idea here is in dependency argument in the end and not in the beginning.

In my comment I was more addressing the whole additional idealogy over that, like special typings and using some pipe, ask, chain. I see these as only a fog and complexity over really simple but powerful concept.

There's a benefit in using the monadic interface though: software is written in a uniform style, regardless of the effect.

You can think of Reader as an effect, muck like Either or Task.

Let's say you have the following snippet

import * as E from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/pipeable'

declare function f(s: string): E.Either<Error, number>
declare function g(n: number): boolean
declare function h(b: boolean): E.Either<Error, Date>

// composing `f`, `g`, and `h` -------------v---------v-----------v
const result = pipe(E.right('foo'), E.chain(f), E.map(g), E.chain(h))

const pointFreeVersion = flow(f, E.map(g), E.chain(h))

and at some point you must refactor f to

import * as RE from 'fp-ts/lib/ReaderEither'

interface Dependencies {
  foo: string
}

declare function f(s: string): RE.ReaderEither<Dependencies, Error, number>

result and pointFreeVersion must be refactored as well, fortunately you can use ReaderEither's monadic interface

// before 
const result = pipe(E.right('foo'), E.chain(f), E.map(g), E.chain(h))

const pointFreeVersion = flow(f, E.map(g), E.chain(h))

// after
const result = pipe(
  RE.right('foo'),
  RE.chain(f),
  RE.map(g),
  RE.chain(b => RE.fromEither(h(b)))
)

const pointFreeVersion = flow(
  f,
  RE.map(g),
  RE.chain(b => RE.fromEither(h(b)))
)

There's a benefit in using the monadic interface though: software is written in a uniform style, regardless of the effect.

I was just about to say it: I think main benefit is (ironically) readability - by saying explicitly Reader<Dependencies, string> you clearly communicate your intent (assuming others know the concept as well, of course 😄).

That's right, you write programs by composing kleisli arrows A -> M<B>, for some effect M.

So Reader (or ReaderEither, ReaderTaskEither, Option, Either, etc...) is just one of the possible effects

 

Wow, this looks great! 😍
I'm having trouble understanding how the Reader monad can help in more complex examples though.
Take a look at this for instance: (functions with the "2" suffix are the ones that I would write having dependencies as the first parameter)

export interface Dependencies {
  readonly i18n: {
    readonly true: string
    readonly false: string
  }
  readonly lowerBound: number
}

export interface OtherDependencies {
  readonly semicolon: boolean
}

// Transform:
const transform = (a: string) => (deps: OtherDependencies) =>
  `myString${deps.semicolon ? ':' : ''} ${a}`

const transform2 = (deps: OtherDependencies) => (a: string) =>
  `myString${deps.semicolon ? ':' : ''} ${a}`

// AnotherTransform:
const anotherTransform = (a: string) => (deps: OtherDependencies) =>
  `${a}${deps.semicolon ? ':' : ''} myString`

const anotherTransform2 = (deps: OtherDependencies) => (a: string) =>
  `${a}${deps.semicolon ? ':' : ''} myString`

// F:
const f = (b: boolean) => (deps: Dependencies) =>
  pipe(
    b,
    ifElse(equals(true), always(deps.i18n.true), always(deps.i18n.false))
  )

const f2 = (deps: Dependencies) =>
  ifElse(equals(true), always(deps.i18n.true), always(deps.i18n.false))

// G:
const g = (n: number) => (deps: Dependencies & OtherDependencies) =>
  pipe(
    n > 2,
    f,
    ff => ff(deps),
    s => transform(s)(deps),
    s => anotherTransform(s)(deps)
  )

const g2 = (deps: Dependencies & OtherDependencies) =>
  flow(
    (n: number) => n > 2,
    f2(deps),
    transform2(deps),
    anotherTransform2(deps)
  )

In particular, notice how:

  1. f needs to make b explicit even if could be implicit (like it is in f2)
  2. all the functions in the g pipe need to take the result from the previous computation and pass it as the first parameter to the transform functions, while this is implicit in g2. This leads to less readable code and harder composition. Is there something I'm missing?
  3. Is it generally right to merge all the dependencies of the functions used by g like I did with Dependencies & OtherDependencies?
 
import { pipe } from 'fp-ts/lib/pipeable' // v2.6
import { chainW, Reader } from 'fp-ts/lib/Reader'

export interface Dependencies {
  readonly i18n: {
    readonly true: string
    readonly false: string
  }
  readonly lowerBound: number
}

export interface OtherDependencies {
  readonly semicolon: boolean
}

declare function transform(a: string): Reader<OtherDependencies, string>
declare function anotherTransform(a: string): Reader<OtherDependencies, string>
declare function f(b: boolean): Reader<Dependencies, string>

const g = (n: number) => pipe(f(n > 2), chainW(transform), chainW(anotherTransform))
 

That's exactly what I needed. I saw you just released chainW in 2.6.0. Great job thanks! :)

 

Do we have a standard name for (r: Reader<R,A>) => A?

import { Reader } from 'fp-ts/lib/Reader';

type Provider<R> = <A>(r: Reader<R,A>) => A

type Name = string
type Age = number

type Customer = {
  name: Name,
  age: Age,
}

const john: Provider<Customer> = (r) => r({
  name: 'John Doe',
  age: 42,
})

const getAge: Reader<Customer, Age> = ({age}) => age

const ageJohn: Age = john(getAge);
 

Yoneda? Provider<R> is isomorphic to R