Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Reader usage example #1049

Closed
vicrac opened this issue Dec 13, 2019 · 13 comments
Closed

Add Reader usage example #1049

vicrac opened this issue Dec 13, 2019 · 13 comments

Comments

@vicrac
Copy link
Contributor

vicrac commented Dec 13, 2019

📖 Documentation

It would be great to have some minimal example usage of Reader monad. As far as I understand, it's purpose is to avoid threading arguments through multiple functions in order only to get them where they belong (at the bottom of calls). It would be nice if there's an example of how to refactor e.g. such a code using Reader:

interface Config {
  // ...
  logLevel: number;
  // ...
}

function foo(config: Config) {
  // ...
  return bar(config)  // `bar` doesn't really needs it, it only passes it further
}

function bar(config: Config) {
  // ...
  baz(config)
  // ...
}

function baz(config: Config) {
  // ...
  if(config.logLevel > 5) writeToLogFile("Error occured in baz")
  // ...
}
@steida
Copy link
Contributor

steida commented Dec 14, 2019

Meanwhile http://learnyouahaskell.com/for-a-few-monads-more

@mlegenhausen
Copy link
Collaborator

As far as I understand, it's purpose is to avoid threading arguments through multiple functions in order only to get them where they belong (at the bottom of calls).

Thats the high level benefit. I would describe it like you can use a function without calling it or a Reader is just a monad for all functions of type (a: A) => B. The later one is important to understand then you see Readers everywhere.

Here a simple example. Please not that ReaderTaskEither<R, E, A> is just an alias for Reader<R, TaskEither<E, A>>.

import * as Rr from 'fp-ts/lib/Reader'
import * as RTE from 'fp-ts/lib/ReaderTaskEither'
import * as TE from 'fp-ts/lib/TaskEither'
import * as b from 'fp-ts/lib/boolean'
import { pipe } from 'fp-ts/lib/pipeable'

interface Config {
  logLevel: number
}

const logLevel: Rr.Reader<Config, number> = config => config.logLevel

declare const foo: RTE.ReaderTaskEither<Config, Error, void>
const bar: RTE.ReaderTaskEither<Config, Error, void> = pipe(
  logLevel,
  Rr.map(logLevel =>
    pipe(
      logLevel > 5,
      b.fold(
        () => TE.right(undefined),
        () => TE.rightIO(() => console.log('Some side effect'))
      )
    )
  )
)

const main: RTE.ReaderTaskEither<Config, Error, void> = pipe(
  foo,
  RTE.chain(() => bar)
)

RTE.run(main, {
  logLevel: 10
})

@vicrac
Copy link
Contributor Author

vicrac commented Dec 17, 2019

@mlegenhausen thanks. Hmmm, all the ReaderTaskEither stuff in this example is a bit overwhelming though. Could you please simplify it a bit more, to keep only the stuff essential for Reader use case? Perhaps some analogy to React Context (I assume, they kind of serve the same purpose - avoiding passing arguments multiple levels in function/componenst tree) would be helpful too (I've seen ask/asks functions in fp-ts and I have really no idea on how to use them - no arguments?). Thanks in advance 😄

@gcanti
Copy link
Owner

gcanti commented Dec 17, 2019

@vicrac @mlegenhausen what about:


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 parameters 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'

@vicrac
Copy link
Contributor Author

vicrac commented Dec 17, 2019

@gcanti Thanks for great explanation 😄 I'd still have one thing confusing me: what does really ask do? If I wrote it this way:

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

wouldn't it be exactly the same? Isn't ask just function returning identity function?

@gcanti
Copy link
Owner

gcanti commented Dec 17, 2019

@vicrac it doesn't compile

Argument of type 'Dependencies' is not assignable to parameter of type 'Reader<Dependencies, unknown>'

@vicrac
Copy link
Contributor Author

vicrac commented Dec 17, 2019

So ask is merely a wrapper for proper type inference?

@gcanti
Copy link
Owner

gcanti commented Dec 17, 2019

No, your g doesn't compile because the types don't align.

You can rewrite g as

// this compiles                                            v-- no chain here
const g = (n: number) => (deps: Dependencies) => pipe(deps, f(n > deps.lowerBound))

I mean, that's fine, however if you come from this g

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

and at some point you want to access the environment, you may want to tweak only the implementation rather than the signature, so ask helps here

//        v-- signature stay the same ------------v
const g = (n: number): Reader<Dependencies, string> =>
  // while the implementation changes
  pipe(
    ask<Dependencies>(),
    chain(deps => f(n > deps.lowerBound))
  )

EDIT: in conclusion 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))
  )

@vicrac
Copy link
Contributor Author

vicrac commented Dec 17, 2019

@gcanti Thanks, that makes a lot of thing clearer 😄 Another question though:

How does mapping on Reader fit into this example? I understand Reader<A, R> as a Monad for function A => R, so what are the monadic operations (map, chain, of) precisely responsible for? Could you describe it?

@gcanti
Copy link
Owner

gcanti commented Dec 18, 2019

@vicrac not sure I understand the question, they are the usual monadic operations.

They are responsible for the same things they are responsible for in the case of, say, Either (or any other monad really): function composition.

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)))
)

p.s.

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

import * as R from 'fp-ts/lib/Reader'
import { flow } from 'fp-ts/lib/function'

declare function len(s: string): number
declare function double(n: number): number
declare function gt2(n: number): boolean

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

@gcanti
Copy link
Owner

gcanti commented Dec 18, 2019

btw this is basically boilerplate

RE.chain(b => RE.fromEither(h(b)))

we could add some helper, like

// ReaderEither.ts

export declare function chainEither<E, A, B>(
  f: (a: A) => E.Either<E, B>
): <R>(ma: RE.ReaderEither<R, E, A>) => RE.ReaderEither<R, E, B>

so we can do

const pointFreeVersion = flow(
  f,
  RE.map(g),
  RE.chainEither(h)
)

What do you think? I'll open a PR

@vicrac
Copy link
Contributor Author

vicrac commented Dec 18, 2019

I think that would be great 😄 Ok, it seems very clear to me now. I'll give it some try and dig around on how I can use it in my projects, and when I come to some conclusion, I'll open a PR adding docs to Reader as well 😄

Btw. all the stuff you just wrote looks like a perfect base for an article on how it works and how to use Reader in fp-ts. Perhaps I'm missing something but I've not found anything about this e.g. on DEV blogs (thanks for great series on fp with fp-ts you've posted there) - it would be really cool to add it so anyone can read it, without digging in Github issues 🚀

@gcanti
Copy link
Owner

gcanti commented Dec 18, 2019

@vicrac yeah, I'll publish my first comment as a blog post as it is (too many things to do..). Perhaps I'll find the time to add some details in the next few weeks

gcanti added a commit that referenced this issue Dec 18, 2019
@gcanti gcanti closed this as completed in 65828f7 Dec 18, 2019
gcanti added a commit that referenced this issue Dec 21, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants