Skip to content

Instantly share code, notes, and snippets.

@ruizb
Last active September 26, 2023 20:21
Show Gist options
  • Star 45 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ruizb/554c17afb9cd3dedc76706862a9fa035 to your computer and use it in GitHub Desktop.
Save ruizb/554c17afb9cd3dedc76706862a9fa035 to your computer and use it in GitHub Desktop.
Reader monad example using fp-ts

This is an adapted version of the Reader documentation from monet.js using fp-ts v2.1.2 and TypeScript v3.5.3.


Reader

The Reader monad is a wonderful solution to inject dependencies into your functions.

The Reader monad provides a way to "weave" your configuration throughout your programme.

Creating a Reader

Say you had this function which requires configuration:

interface Printer {
  write: (message: string) => string
}

const createPrettyName = (name: string, printer: Printer) => printer.write(`hello ${name}`)

Calling this function from other functions that don't need the dependency printer is kind of awkward.

const render = (printer: Printer) => createPrettyName('Tom', printer)

One quick win would be to curry the createPrettyName function, and make render partially apply the function and let the caller of render supply the printer.

const createPrettyName = (name: string) => (printer: Printer) => printer.write(`hello ${name}`)

const render = createPrettyName('Tom') // render(printer)

This is better, but what if render wants to perform some sort of operation on the result of createPrettyName? It would have to apply the final parameter (i.e. the printer) before createPrettyName would execute.

This is where the Reader monad comes in. We could rewrite createPrettyName thusly:

import * as R from 'fp-ts/lib/Reader'

const createPrettyName = (name: string): R.Reader<Printer, string> =>
  (printer: Printer) => printer.write(`hello ${name}`)

⚠️ NOTE: if you are using fp-ts v2.8.0 or more, you should import fp-ts modules without the /lib part. For example, here we would have import * as R from 'fp-ts/Reader'.

So now, when a name is supplied to createPrettyName the Reader monad is returned, and being a monad it supports all the monadic goodness.

We can now get access to the result of createPrettyName through a map.

const reader = R.reader.map(createPrettyName('Tom'), (s: string) => `---${s}---`)

The top level of our program would co-ordinate the dependency injection by calling the resulting Reader with said dependency:

reader(new BoldPrinter())

The following section is not part of monet.js documentation, but I think it's worth showing how we can compose readers using fp-ts.

Reader composition

Let's say we have the following piece of code:

interface Dependencies {
  logger: { log: (message: string) => void }
  env: 'development' | 'production'
}

const c = ({ logger, env }: Dependencies) => {
  logger.log(`[${env}] calling c function`)
  return 1
}
const b = (deps: Dependencies) => c(deps) * 2
const a = (deps: Dependencies) => b(deps) + 1

const logger = { log: (message: string) => console.log(message) }
const deps: Dependencies = { logger, env: 'development' }
const result = a(deps)

assert(result === 3)

(I voluntarily didn't use IO for the logger to avoid adding complexity to this example)

As you can see, a and b must have knowledge about c dependencies despite not using them. This adds "noise", making the code more complex, thus decreasing its readability and maintainability.

Using Reader can improve this part:

import * as R from 'fp-ts/lib/Reader'

interface Dependencies {
  logger: { log: (message: string) => void }
  env: 'development' | 'production'
}

const c = ({ logger, env }: Dependencies) => {
  logger.log(`[${env}] calling c function`)
  return 1
}
const b = R.reader.map(c, n => n * 2)
const a = R.reader.map(b, n => n + 1)
// a, b and c types are the same: Reader<Dependencies, number>

const logger = { log: (message: string) => console.log(message) }
const deps: Dependencies = { logger, env: 'development' }
const result = a(deps)

assert(result === 3)

⚠️ NOTE: if you are using fp-ts v2.8.0 or more, you should import fp-ts modules without the /lib part. For example, here we would have import * as R from 'fp-ts/Reader'.

Now, a and b have no knowledge about the dependencies necessary to make c work, which is what we are looking for. However, we still have some boilerplate due to Reader: the R.reader.map(ma, X) is common to both a and b and is not related to their main logic: (respectively) adding 1 to a given number and doubling a given number.

Let's see if we can isolate the logic of a and b while keeping the advantages of Reader:

import * as R from 'fp-ts/lib/Reader'
import { pipe } from 'fp-ts/lib/pipeable'

interface Dependencies {
  logger: { log: (message: string) => void }
  env: 'development' | 'production'
}

const c = ({ logger, env }: Dependencies) => {
  logger.log(`[${env}] calling c function`)
  return 1
}
const b = (n: number) => n * 2
const a = (n: number) => n + 1

const logger = { log: (message: string) => console.log(message) }
const deps: Dependencies = { logger, env: 'development' }
const result = pipe(
  R.of(a),
  R.ap(pipe(
    R.of(b),
    R.ap(c)
  ))
)(deps)

assert(result === 3)

This is what's happening when using pipe:

  1. First, we lift the a function into a Reader using R.of:

    R.of((n: number) => number) -> Reader<Dependencies, (n: number) => number>

  2. Then, inside the Reader context, we call a with the result of calling b using R.ap:

    R.ap(Reader<Dependencies, number)(Reader<Dependencies, (n: number) => number>) -> Reader<Dependencies, number>

  3. To get the result of b, we do the following:

  4. Lift the b function into a Reader using R.of:

    R.of((n: number) => number) -> Reader<Dependencies, (n: number) => number>

  5. Inside the Reader context, call b with the result of calling c using R.ap:

    R.ap(Reader<Dependencies, number)(Reader<Dependencies, (n: number) => number>) -> Reader<Dependencies, number>

So far we didn't execute anything, but we prepared the execution. By using pipe, we basically created this function: deps => a(b(c(deps))). Calling pipe(...)(deps) is the same as calling (deps => a(b(c(deps))))(deps), which runs the reader that triggers the execution of the program.

The result const could've been written the following way:

const result = pipe(
  c,
  R.map(b),
  R.map(a)
)(deps)

This is actually the piped version of (deps => a(b(c(deps))))(deps).

Do note that using the Reader monad in this example, the order of execution is inverted: previously, when calling a(deps) we were first calling a, then b and finally c. Using Reader we are actually calling c first in order to provide the result to b, then calling b to ultimately provide the result to a.

If you wish to preserve the original order of execution though, I don't think it's possible to use composition tools such as pipe, but you can still use Reader to abstract c dependencies:

import * as R from 'fp-ts/lib/Reader'

interface Dependencies {
  logger: { log: (message: string) => void }
  env: 'development' | 'production'
}

const c = ({ logger, env }: Dependencies) => {
  console.log('third')
  logger.log(`[${env}] calling c function`)
  return env === 'development' ? 1 : 0
}

const b = () => { // `b` is still unaware of `c` dependencies
  console.log('second')
  return R.reader.map(c, _ => _ * 2)
}

const a = () => { // `a` is still unaware of `c` dependencies
  console.log('first')
  const mustRunHeavyComputation = true
  return mustRunHeavyComputation
         ? R.reader.map(b(), _ => _ + 1)
         : R.reader.of(-1)
}

const logger = { log: (message: string) => console.log(message) }
const deps: Dependencies = { logger, env: 'development' }

// Logs 'first', then 'second' and finally 'third' messages, in that order
const result = a()(deps)

assert(result === 3)
@snatvb
Copy link

snatvb commented Aug 19, 2021

Thanks a lot!

@guillaumeagile
Copy link

Hi.
I try your code but R.reader is deprecated. What's the new way?

@davidc6
Copy link

davidc6 commented Mar 18, 2022

Hi. I try your code but R.reader is deprecated. What's the new way?

Hi @guillaumeagile. You should be able to import functions/types in fp-ts v2 using:

import { map, of, Reader } from 'fp-ts/Reader'

or

import * as R from 'fp-ts/Reader'

R.map()
R.of()
R.Reader

@ruizb
Copy link
Author

ruizb commented Mar 31, 2022

Hey there, I added a note for people using fp-ts v2.8.0+ regarding the way modules should be imported. Thanks for the feedback!

@guillaumeagile
Copy link

Nice. Thanks.

@palanom
Copy link

palanom commented Nov 22, 2022

Hi. I try your code but R.reader is deprecated. What's the new way?

Hi @guillaumeagile,
I assume you are referring to R.reader.map, in that case since .map is a function belonging to the Functors, you find it as R.Functor.map (while the .chain that is a Monad function is available in R.Monad.chain)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment