Reader monads
Tags: functional, typescript, fpts
Published: 2023-06-04 12:53:15

At the end of the [previous post], there was a code example that could be improved in several ways. One of them was using dependency injection to improve its testability. In this post, we learn how to do just that using the reader monads.

The reader monad represents a computation that can _read_ values from an environment, or context, and return a value. The `Reader` interface is declared the following way<sup>[[1]]</sup>:

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

`R` represents the environment and `A` is the return value. In this post, we use the following terms to refer to R: "R type", "environment", "dependencies".

Similar to how [dependency injection] is used in object-oriented programming, the Reader monad serves as a mechanism for managing and accessing dependencies in functional programming. It abstracts away the manual handling of dependencies by implicitly threading them through the computation. Let's demonstrate this on a small example.

```ts
import { pipe } from "fp-ts/lib/function.js";

interface Logger {
  debug: (msg: string) => void;
}

const logger: Logger = {
  debug: (msg) => console.debug(msg),
};

const addWithLogging = (a: number) => ({ debug }: Logger) => (b: number): number => {
  debug(`Adding ${b} to ${a}`);

  return a + b;
}

const res = pipe(
  39,
  addWithLogging(1)(logger),
  addWithLogging(2)(logger),
);

console.log({ res });
```

Notice the code duplication inside the pipe and how we manually pass down the logger to both calls of `addWithLogging`. This can be solved by the adoption of the Reader monad and its functions like so:

```ts
import { reader } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";

interface Logger {
  debug: (msg: string) => void;
}

const logger: Logger = {
  debug: (msg) => console.debug(msg),
};

const addWithLogging = (a: number) => (b: number): reader.Reader<Logger, number> => ({ debug }) => {
  debug(`Adding ${b} to ${a}`);

  return a + b;
}

const res = pipe(
  39,
  addWithLogging(1),
  reader.chain(addWithLogging(2)),
)(logger);

console.log({ res });
```

Notice how we rearranged the order of arguments in `addWithLogging` to help us make the function more composable using `reader.chain`. With this, we can pass the logger to the computation in a single place only.

## Types of Reader monads

Depending on the type of computation, there are different "flavors" of the Reader monad:

- `Reader`: represents a synchronous operation that depends on a certain environment.
- `ReaderTask`: represents an asynchronous operation that does not fail and depends on a certain environment. Syntactic sugar over `Reader<R, Task<A>>`. Basically, it is a [Task] with a declared dependency.
- `ReaderTaskEither`: represents an asynchronous operation that can fail and depends on a certain environment. Syntactic sugar over `Reader<R, TaskEither<E, A>>`. Basically, it is a [TaskEither] with a declared dependency.
- etc.

## Conversions

It is possible to convert one Reader monad to another. The following table shows how to perform a couple of such conversions:

| From | To | Converter |
| ---- | -- | --------- |
| Task | ReaderTask | `readerTask.fromTask` |
| Reader | ReaderTask | `readerTask.fromReader` |
| Option | ReaderTaskEither | `readerTaskEither.fromOption` |
| Either | ReaderTaskEither | `readerTaskEither.fromEither` |
| Task | ReaderTaskEither | `readerTaskEither.fromTask` |
| TaskEither | ReaderTaskEither | `readerTaskEither.fromTaskEither` |
| Reader | ReaderTaskEither | `readerTaskEither.fromReader` |

## Reader-specific functions

The family of Reader monads implements a couple of functions that no other monad does. In this section, we take a brief look at some of them.

### asks

`reader.asks` lets us tap into the environment and describe (not execute) a computation. It is defined as<sup>[2]</sup>:

```ts
import { reader } from "fp-ts";

export declare const asks: <R, A>(
  f: (r: R) => A
  ) => reader.Reader<R, A>;
```

`asks` basically takes a function of `Reader<R, A>` and returns a `Reader<R, A>`. It may seem as if it returned itself. However, in practice, we may use this to tap into the environment, perform a computation, and return the result as a reader. This becomes particularly useful in combination with the `Do` notation and `bind`ing values.

The Do notation is basically just a pipe that starts with `reader.Do` which merely initializes an empty object for us to be the target of the `bind` operations that follow. Several monads implement the Do notation, it is not specific to the Reader monad.

```ts
import { random as _random, reader } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";

interface Random {
  next: () => number;
}

interface Env {
  random: Random;
}

const nextRandom: reader.Reader<Env, number> = ({
  random
}) => random.next();

export const chanceButLower: reader.Reader<Env, number> = pipe(
  reader.Do, // returns reader.Reader<unknown, {}>
  reader.bind("a", () => reader.asks(nextRandom)),
  reader.bind("b", () => reader.asks(nextRandom)),
  reader.map(({ a, b }) => a * b)
);

// Example usage
console.log({
  result: chanceButLower({ random: { next: _random.random } }),
});
```

In the above example, the `chanceButLower` function depends on an environment containing a random number generator service. It generates two random numbers between 0 and 1, multiplies them, and returns the result.

### local

Normally, we can only "read" the dependencies from within a reader. However, sometimes we may run into situations where we need to modify the `R` value before executing a chained computation. An example situation is when we want to compose two readers that have completely different dependencies. This is where `reader.local` comes into play. It is defined as<sup>[3]</sup>:

```ts
import type { reader } from "fp-ts";

export declare const local: <R2, R1>(
  f: (r2: R2) => R1
) => <A>(ma: reader.Reader<R1, A>) => reader.Reader<R2, A>;
```

It takes two arguments: (1) a function that maps one R value to another, and (2) a reader of the R value returned by the first argument. The function itself returns a reader of the initial R value. The easiest way to think about this function is that it modifies the environment (context) for the local execution of its second argument. The ability of changing the environment for local function executions can help us compose readers of different `R` types.

```ts
import { random as _random, reader } from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function.js";

interface Random {
  next: () => number;
}

const chanceButLower: reader.Reader<Random, number> = ({
  next
}) => next() * next();

type Result = "WIN" | "LOSE";

interface SlotMachine {
  result: (chance: number) => Result;
}

const predict = (
  chance: number
): reader.Reader<SlotMachine, Result> => ({
  result
}) => result(chance);

interface Deps {
  random: Random;
  slotMachine: SlotMachine;
}

const tryMyLuck: reader.Reader<Deps, Result> = pipe(
  chanceButLower,
  reader.local((deps: Deps) => deps.random),
  reader.chain(
    flow(
      predict,
      reader.local((deps: Deps) => deps.slotMachine)
    )
  )
)

// Example usage
console.log({
  result: tryMyLuck({
    random: { next: _random.random },
    slotMachine: { result: (chance) => chance >= 0.99 ? "WIN" : "LOSE" }
  })
});
```

In the above example, we have two functions that each depend on a different `R` type. The `chanceButLower` function depends on a `Random` service, while the `predict` function does so on a `SlotMachine` service. The `tryMyLuck` function composes these two functions by using `reader.local` to map the environment for each function accordingly. This way, `tryMyLuck` depends on a combined environment of the two composed functions.

However, in this particular case, we can completely eliminate the need to use `reader.local`, and make the composition significantly simpler. To achieve that, we need to adopt a simple rule: all `R` types representing dependencies must be interface types, even if they contain just a single dependency.

```ts
import { random as _random, reader } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";

interface Random {
  next: () => number;
}

const chanceButLower: reader.Reader<{ random: Random }, number> = ({
  random
}) => random.next() * random.next();

type Result = "WIN" | "LOSE";

interface SlotMachine {
  result: (chance: number) => Result;
}

const predict = (
  chance: number
): reader.Reader<{ slotMachine: SlotMachine }, Result> => ({
  slotMachine
}) => slotMachine.result(chance);

interface Deps {
  random: Random;
  slotMachine: SlotMachine;
}

const tryMyLuck: reader.Reader<Deps, Result> = pipe(
  chanceButLower,
  reader.chainW(predict)
)

// Example usage
console.log({
  result: tryMyLuck({
    random: { next: _random.random },
    slotMachine: { result: (chance) => chance >= 0.99 ? "WIN" : "LOSE" }
  })
});
```

Notice how much simpler the `tryMyLuck` function becomes compared to the previous code example. Because of reader's `chainW` the first function's environment is widened to the final (combined) environment.

As long as we follow the "R type convention" for dependencies, and use the same key to refer to the same dependency across the project, functions returning a `Reader` become trivial to compose.

## Practical code example

Now that we know how to manage dependencies using the `Reader` monad, we are ready to fix the shortcomings of the [example code from the previous post](https://dev.to/attila_vecerek/task-monads-2ob0#practical-code-example). Let's start by updating the `Project` domain file.

```ts
// domains/project.ts

import type { either, readerTaskEither } from "fp-ts";
import type * as db from "./db.js";
import type * as kafka from "./kafka.js";

export interface Project {
  id: string;
  name: string;
  description: string;
  organizationId: string;
}

export type ProjectInput = Pick<
  Project,
  "name" | "description" | "organizationId"
>;

export class ParseInputError {
  readonly _tag = "ParseInputError";
  constructor(readonly error: Error) {}
}

export class ProjectNameUnavailableError {
  readonly _tag = "ProjectNameUnavailableError";
  constructor(readonly error: Error) {}
}

export type ValidateAvailabilityError =
  | ProjectNameUnavailableError
  | db.QueryError;

export class ProjectLimitReachedError {
  readonly _tag = "ProjectLimitReachedError";
  constructor(readonly error: Error) {}
}

export type EnforceLimitError = ProjectLimitReachedError | db.QueryError;

export class MessageEncodingError {
  readonly _tag = "MessageEncodingError";
  constructor(readonly error: Error) {}
}

export type EmitEntityError = MessageEncodingError | kafka.ProducerError;

/**
 * A synchronous operation that accepts an unknown object
 * and parses it. The result is an `Either`.
 */
export type ParseInput = (
  input: unknown
) => either.Either<ParseInputError, ProjectInput>;
export declare const parseInput: ParseInput;

export interface DbEnv {
  db: db.Service
}

/**
 * A function that accepts an object representing
 * the input data of a project and returns a ReaderTaskEither
 * describing an asynchronous operation which queries
 * the database to check whether the project name
 * is still available. Project names across an organization
 * must be unique. This operation may fail due to different
 * reasons, such as network errors, database connection
 * errors, SQL syntax errors, etc. The database client
 * is the only dependency of this function.
 */
export type ValidateAvailability = (
  input: ProjectInput
) => readerTaskEither.ReaderTaskEither<DbEnv, ValidateAvailabilityError, void>;
export declare const validateAvailability: ValidateAvailability;

/**
 * A task describing an asynchronous operation that
 * queries the database for the number of existing
 * projects for a given organization. There is a
 * product limit for how many projects can be created
 * by an organization. This operation fails if the limit
 * is reached or any other network or database error occurs.
 * The database client is the only dependency of this function.
 */
export type EnforceLimit = readerTaskEither.ReaderTaskEither<DbEnv, EnforceLimitError, void>;
export declare const enforceLimit: EnforceLimit;

/**
 * A function that accepts a project object and returns
 * a task describing an asynchronous operation that
 * persists this object in the database. This operation
 * fails if any network or database error occurs.
 * The database client is the only dependency of this function.
 */
export type Create = (
  project: Project
) => readerTaskEither.ReaderTaskEither<DbEnv, db.QueryError, void>;
export declare const create: Create;

/**
 * A function that accepts a project object and returns
 * a task describing an asynchronous operation that encodes
 * this object and produces a Kafka message. This operation
 * fails if the encoding fails, or any other network or broker
 * error occurs. The database client is the only dependency
 * of this function.
 */
export type EmitEntity = (
  project: Project
) => readerTaskEither.ReaderTaskEither<DbEnv, EmitEntityError, void>;
export declare const emitEntity: EmitEntity;
```

As we can see, all we did was we replaced `taskEither.TaskEither` with `readerTaskEither.ReaderTaskEither`, declared a type for the database dependency, and referenced it from all the necessary places. Now, we're ready to update the `Project` repository file containing the composition of the domain functions.

```ts
// repositories/project.ts

import { readerTaskEither } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { ulid } from "ulid";
import * as project from "../domains/project.js";

export const create = flow(
  project.parseInput,
  readerTaskEither.fromEither,
  readerTaskEither.chainFirstW(project.validateAvailability),
  readerTaskEither.chainFirstW(() => project.enforceLimit),
  readerTaskEither.bind("id", () => readerTaskEither.right(ulid())),
  readerTaskEither.chainFirstW(project.create),
  readerTaskEither.chainFirstW(project.emitEntity)
);
```

As we can see, the update here was just as simple as the one above if not even simpler.

Let's take a look at another shortcoming, the lack of transaction control. To address that, we need to implement a `withTransaction` function that takes a computation and makes sure that it starts a transaction before the computation is executed and that it commits and rolls back the transaction according to the result of the computation. After the implementation, this is how the file implementing our database service looks like:

```ts
import { either, readerTaskEither, taskEither } from "fp-ts";
import { constVoid, pipe } from "fp-ts/lib/function.js";

export class QueryError {
  readonly _tag = "QueryError";
  constructor(readonly error: Error) {}
}

export interface Service {
  query: (sql: string) => (values: unknown[]) => taskEither.TaskEither<QueryError, unknown>;
}

export class StartTransactionFailedError {
  readonly _tag = "StartTransactionFailedError";
  constructor(readonly error: Error) {}
}

export class RollbackFailedError {
  readonly _tag = "RollbackFailedError";
  constructor(readonly error: Error) {}
}

export class CommitFailedError {
  readonly _tag = "RollbackFailedError";
  constructor(readonly error: Error) {}
}

export type WithTransactionError = CommitFailedError | QueryError | RollbackFailedError | StartTransactionFailedError;

export interface WithTransaction {
  <R, E, A>(
    use: readerTaskEither.ReaderTaskEither<R, E, A>
  ): readerTaskEither.ReaderTaskEither<R & { db: Service }, E | WithTransactionError, A>
}
export const withTransaction: WithTransaction = (use) => (env) => taskEither.bracketW(
  pipe(
    [],
    env.db.query("START TRANSACTION"),
    taskEither.mapLeft(({ error }) => new StartTransactionFailedError(error))
  ),
  () => use(env),
  (_, res) =>
    pipe(
      res,
      either.match(
        () =>
          pipe(
            [],
            env.db.query("ROLLBACK"),
            taskEither.mapLeft(({ error }) => new RollbackFailedError(error)),
            taskEither.map(constVoid)
          ),
        () =>
          pipe(
            [],
            env.db.query("COMMIT"),
            taskEither.mapLeft(({ error }) => new CommitFailedError(error)),
            taskEither.map(constVoid)
          )
      )
    )
);
```

The `withTransaction` function takes a single argument (`use`) representing an async computation that can fail and depends on a certain environment. The return value of this function is another async computation that can also fail and depends on a combined environment of the `use` function and its own environment (`{ db: Service }`). It is implemented using `taskEither.bracket`. We can think of `bracket` as a resource manager that works in three steps:

1. It acquires a resource. We simply execute a `START TRANSACTION` query.
2. It performs a computation, optionally by using the acquired resource. In this particular case, we throw the value of the resource away.
3. It releases the resource. In this step, `bracket` provides the acquired resource and the result of the computation from the previous step to the caller. We inspect the result:
  - in the case of a failure, we execute the `ROLLBACK` query,
  - otherwise, we execute the `COMMIT` query.

To wrap our composed `project.create` function in a transaction, all we need to do is to call `withTransaction` as the last step of the pipeline like so:

```ts
// repositories/project.ts

import { readerTaskEither } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { ulid } from "ulid";
import * as project from "../domains/project.js";
import * as db from "../db.js";

export const create = flow(
  project.parseInput,
  readerTaskEither.fromEither,
  readerTaskEither.chainFirstW(project.validateAvailability),
  readerTaskEither.chainFirstW(() => project.enforceLimit),
  readerTaskEither.bind("id", () => readerTaskEither.right(ulid())),
  readerTaskEither.chainFirstW(project.create),
  readerTaskEither.chainFirstW(project.emitEntity),
  db.withTransaction
);
```

Just like in the previous post, the type of the `create` function gets widened again. This time by the additional `db.WithTransactionError` error type:

```ts
import type { readerTaskEither } from "fp-ts";
import type * as project from "../domains/project.js";
import type * as db from "../db.js";

export interface Create {
  (input: unknown): readerTaskEither.ReaderTaskEither<
    project.DbEnv,
    | db.WithTransactionError
    | db.QueryError
    | project.ParseInputError
    | project.ProjectNameUnavailableError
    | project.ProjectLimitReachedError
    | project.EmitEntityError,
    project.Project
  >;
}
```

## Wrap-up

- The `Reader` monad serves as a mechanism for managing and accessing dependencies.
- `reader.asks` allows us to access the dependencies, perform a computation, and return its result as another reader of the same dependencies.
- `reader.local` allows us to compose readers of different dependencies.
- Reader monads compose much easier when we adopt the convention of representing dependencies through an interface type (even if the reader only declares a single dependency) and we give our dependencies a consistent key across the whole project.
- Depending on the result type, there can be different "flavors" of the Reader monad, for example `ReaderEither` for readers returning an `Either`.
- It is easy to convert between instances of different "flavors" of reader monads.
- It is easier to reason about programs using Reader monads because it is immediately clear what dependencies must be satisfied by the caller.
- It is easier to test code that uses Reader monads the same way it is easier to test object-oriented code that uses dependency injection.

The next post of this series is centered around runtime type-systems and how to use them to build programs with end-to-end type safety.

## Extra resources

- [Getting started with fp-ts: Reader]
- [Reader, Writer, and State Monads in TypeScript with fp-ts]
- [All code samples in this post are available on Github]

[dependency injection]: https://en.wikipedia.org/wiki/Dependency_injection
[previous post]: https://dev.to/attila_vecerek/task-monads-2ob0
[1]: https://github.com/gcanti/fp-ts/blob/9da2137efb295b82fb59b7b0c2114f2ceb40a2b5/src/Reader.ts#L67-L69
[Task]: https://dev.to/attila_vecerek/task-monads-2ob0#task
[TaskEither]: https://dev.to/attila_vecerek/task-monads-2ob0#taskeither
[2]: https://github.com/gcanti/fp-ts/blob/8c4e31d79d96196738d8b1bbc45a98fc3ff8f3d6/src/Reader.ts#L83-L89
[3]: https://github.com/gcanti/fp-ts/blob/9da2137efb295b82fb59b7b0c2114f2ceb40a2b5/src/Reader.ts#L125-L126
[Getting started with fp-ts: Reader]: https://dev.to/gcanti/getting-started-with-fp-ts-reader-1ie5
[Reader, Writer, and State Monads in TypeScript with fp-ts]: https://levelup.gitconnected.com/reader-writer-and-state-monad-with-fp-ts-6d7149cc9b85
[All code samples in this post are available on Github]: https://github.com/vecerek/blog/tree/main/packages/fp-in-ts/src/06-reader-monads