From 0 to Reader
Published: 2020-09-18 14:27:38

The following notes come from an internal discussion I had with some coworkers with no pretension to be an accurate explanation of the Reader monad. Still, my teammates claimed they were helpful to understand the concept; so better put them online.

---

We'll start with a function whose job is to insert an user in a database:

```ts
type User = {
  username: string;
  age: number;
};

declare function createUser(
  user: string,
  details: unknown
): Promise<User>;

```

Let's write some code to implement the function:

```ts
type User = {
  username: string;
  age: number;
};

declare function userExists(user: string): Promise<boolean>;

declare function createUserAccount(
  user: string
): Promise<boolean>;

declare function runAutomaticTrigger(
  user: string
): Promise<boolean>;

async function insertInDb(user: User): Promise<boolean> {
  const db = [];
  db.push(user);

  return runAutomaticTrigger(user.username);
}

async function createUser(details: User): Promise<User> {
  const isPresent = await userExists(details.username);

  if (isPresent) {
    const inserted = await insertInDb(details);

    if (inserted) {
      const accountCreated = await createUserAccount(
        details.username
      );

      if (accountCreated) return details;
      else throw new Error("unable to create user account");
    } else throw new Error("unable to insert user in Db");
  } else {
    throw new Error("user already exists");
  }
}
```

Now let's say that somebody comes says we need to add logging with this object.

```ts
type Logger = {
  info: (msg: string) => undefined,
  debug: (msg: string) => undefined,
  warn: (msg: string) => undefined,
  error: (msg: string) => undefined,
};
```

Additionally, let's put the constraint in place that the logger is not a singleton instance — thus it's an instance that needs to be carried around.

```ts
declare function userExists(user: string, l: Logger): Promise<boolean>;

declare function createUserAccount(user: string, l: Logger): Promise<boolean>;

declare function runAutomaticTrigger(user: string, l: Logger): Promise<boolean>;

async function insertInDb(user: User, l: Logger): Promise<boolean> {
  const db = [];
  db.push(user);

  l.info("User inserted, running trigger");

  return runAutomaticTrigger(user.username, l);
}

async function createUser(details: User): Promise<User> {
  const isPresent = await userExists(details.username, l);

  if (isPresent) {
    const inserted = await insertInDb(details, l);

    if (inserted) {
      const accountCreated = await createUserAccount(details.username, l);

      if (accountCreated) return details;
      else {
        throw new Error("unable to create user account");
      }
    } else {
      throw new Error("unable to insert user in Db");
    }
  } else {
    {
      throw new Error("user already exists");
    }
  }
}
```

Two things aren't really cool with such approach:

1. I have to pass the logger in every single function that needs this — every function must be aware of the new dependency
2. The logger is a *dependency*, not really a function argument.

To start fixing this, let's try to put the dependency elsewhere:

```diff
- declare function userExists(user: string, l: Logger): Promise<boolean>;
+ declare function userExists(user: string): (l: Logger) => Promise<boolean>;
```

So that we change the way we use the function:

```diff
- const promise = userExists(user, logger);
+ const promise = userExists(user)(logger);
```

The result is:

```ts
declare function userExists(user: string): (l: Logger) => Promise<boolean>;

declare function createUserAccount(
  user: string
): (l: Logger) => Promise<boolean>;

declare function runAutomaticTrigger(
  user: string
): (l: Logger) => Promise<boolean>;

function insertInDb(user: User) {
  return (l: Logger) => {
    const db = [];
    db.push(user);

    return runAutomaticTrigger(user.username)(l);
  };
}

async function createUser(details: User) {
  return async (l: Logger) => {
    const isPresent = await userExists(details.username)(l);

    if (isPresent) {
      const inserted = await insertInDb(details)(l);

      if (inserted) {
        const accountCreated = await createUserAccount(details.username)(l);

        if (accountCreated) return details;
        else {
          throw new Error("unable to create user account");
        }
      } else {
        throw new Error("unable to insert user in Db");
      }
    } else {
      {
        throw new Error("user already exists");
      }
    }
  };
}
```

Let's now introduce a type to help us out to model this:

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

And so we can now rewrite `userExists` as:

```diff
- declare function userExists(user: string): (l: Logger) => Promise<boolean>;
+ declare function userExists(user: string): Reader<Logger, Promise<boolean>>;
```

Since TypeScript does not support HKT (but I still pray everyday that eventually it will), I am going to define a more specific type

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

So I can make the following replacement:

```diff
- declare function userExists(user: string): Reader<Logger, Promise<boolean>>;
+ declare function userExists(user: string): ReaderPromise<Logger, boolean>;
```

…and if I define an helper function called chain:

```ts
const chain = <R, A, B>(ma: ReaderPromise<R, A>, f: (a: A) => ReaderPromise<R, B>): ReaderPromise<R, B> => (r) =>
  ma(r).then((a) => f(a)(r))
```

I can now rewrite the entire flow in such way:

```ts
function createUser(details: User): ReaderPromise<Logger, User> {
  return chain(userExists(details.username), (isPresent) => {
    if (isPresent) {
      return chain(insertInDb(details), (inserted) => {
        if (inserted) {
          return chain(createUserAccount(details.username), (accountCreated) => {
            if (accountCreated) {
              return (logger) => Promise.resolve(details);
            } else {
              throw new Error("unable to insert user in Db");
            }
          });
        } else {
          throw new Error("unable to create user account");
        }
      });
    } else {
      throw new Error("user already exists");
    }
  });
}
```

but that ain't that cool, since we're nesting nesting and nesting. We need to move to the next level.

Let's rewrite chain to be curried…

```diff
- const chain = <R, A, B>(ma: ReaderPromise<R, A>, f: (a: A) => ReaderPromise<R, B>): ReaderPromise<R, B> => (r) =>
  ma(r).then((a) => f(a)(r))
+ const chain = <R, A, B>(f: (a: A) => ReaderPromise<R, B>) => (ma: ReaderPromise<R, A>):  ReaderPromise<R, B> => (r) =>
  ma(r).then((a) => f(a)(r))

```

Well what happens now is that I can use ANY implementation of the pipe operator (the one in lodash will do), and write the flow in this way:

```ts
function createUser2(details: User): ReaderPromise<Logger, User> {
  return pipe(
    userExists(details.username),
    chain((isPresent) => {
      if (isPresent) return insertInDb(details);
      throw new Error("user already exists");
    }),
    chain((inserted) => {
      if (inserted) return createUserAccount(details.username);
      throw new Error("unable to create user account");
    }),
    chain((accountCreated) => {
      if (accountCreated) return DoSomething;
      throw new Error("unable to create user account");
    })
  );
}
```

I can introduce another abstraction called `Task`

`type Task<T> = () => Promise<T>`

and then, just for commodity

`type ReaderTask<R, A> = Reader<R, Task<A>>`

Then I can refactor this part a little bit:

```diff
- declare function userExists(user: string): Reader<Logger, Promise<boolean>>;
+ declare function userExists(user: string): ReaderTask<Logger, boolean>;
```

It turns out [fp-ts](https://github.com/gcanti/fp-ts) already has a bunch of these defined, so I'm not going to bother using mines:

```ts
import * as R from "fp-ts/Reader";
import * as RT from "fp-ts/ReaderTask";
import { pipe } from "fp-ts/pipeable";

type User = {
  username: string;
  age: number;
};

type Logger = {
  info: (msg: string) => void;
  debug: (msg: string) => void;
  warn: (msg: string) => void;
  error: (msg: string) => void;
};

declare function userExists(user: string): RT.ReaderTask<Logger, boolean>;
declare function createUserAccount(
  user: string
): RT.ReaderTask<Logger, boolean>;
declare function runAutomaticTrigger(
  user: string
): RT.ReaderTask<Logger, boolean>;

function insertInDb(user: User): RT.ReaderTask<Logger, boolean> {
  const db = [];
  db.push(user);

  return runAutomaticTrigger(user.username);
}

function createUser(details: User): RT.ReaderTask<Logger, Promise<User>> {
  return pipe(
    RT.ask<Logger>(),
    RT.chain(l => userExists(details.username)),
    RT.chain(isPresent => {
      if (isPresent) {
        return insertInDb(details);
      } else {
        throw new Error("user already exists");
      }
    }),
    RT.chain(inserted => {
      if (inserted) {
        return createUserAccount(details.username);
      } else {
        throw new Error("unable to create user account");
      }
    }),
    RT.map(accountCreated => {
      if (accountCreated) {
        return Promise.resolve(details);
      } else {
        throw new Error("unable to insert user in Db");
      }
    })
  );
}
```

What are the differences with the original, naive, solution?

1. Functions are not aware of the dependency at all. You just chain them and inject the dependency once: `const user = await createUser(details)(logger)()`
2. The logger is now a separate set of arguments, making really clear what is a dependency and what is a function argument
3. You can reason about the result of the computation even though you haven't executed anything yet.