The Option monad
Tags: functional, typescript, fpts
Published: 2022-12-06 10:05:22

Just like the Either monad in the [previous post], the `Option` monad also specifies an Option data type and the functions operating on top of it. In this post, we'll also learn that once we know how to work with one monad, we pretty much know how to work with all of them.

The `Option` data type represents a value that _may not be_ present. That's also why in some functional languages this monad is called the `Maybe` monad. Practically speaking, it allows us to get rid of the use of `A | null` and `A | undefined` kind of types. Consequently, it also removes the need for constant null checks in our code. This is how `fp-ts` defines `Option` <sup>[[1]]</sup>:

```ts
interface None {
  readonly _tag: "None";
}

interface Some<A> {
  readonly _tag: "Some";
  readonly value: A;
}

export type Option<A> = None | Some<A>;
```

`Option` is a [tagged union] consisting of the `None` and `Some` types. `None` represents the missing value, whereas `Some` wraps the value of type `A`. In a sense, it is a specialized form of `Either<undefined, A>`. This type is usually returned from lookup functions:

- accessing an index of an array,
- accessing a property of an object,
- looking up a specific record in a database.

## Constructors

The `Option` monad has two basic constructors, one for each tag:

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

export const missing = option.none;
export const present = option.some(42);
```

It can also construct a new `Option` from a nullable type (`A | null | undefined`). Very useful in the inevitable case of working with some popular libraries that do return such values.

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

export const missing = option.fromNullable(null); // None
export const alsoMissing = option.fromNullable(undefined); // None
export const present = option.fromNullable(42); // Some(42)
```

## Mapping

As opposed to Either, the `Option` monad implements a single `map` function. The `None` type does not wrap any value, hence there's nothing to map from. However, there is an exception to this when lifting Option to Either. The concept of lifting is discussed in more details in a later [section](#lifting) of this post.

```ts
import { option, record } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";

interface LookupCount {
  (_: Record<string, number>): option.Option<{ count: number }>;
}

const lookupCount: LookupCount = flow(
  record.lookup("total"),
  option.map((count) => ({ count }))
);

export const res = lookupCount({ total: 42 });
// Value of res is { _tag: "Some", value: { count: 42 } }

export const res2 = lookupCount({});
// Value of res2 is { _tag: "None" }
// Type of both res and res2 is Option<{ count: number }>
```

In the above example, we define a function called `lookupCount` that takes a Record, checks if it contains a property called `total` and maps it to an object of type `{ count: number }` returning a `Some`. If the property does not exist, it returns a `None`.

Our previous examples did not contain manual typings and relied on type inference instead. Unfortunately, in this example, explicitly typing the `lookupCount` function is necessary. It is likely that TypeScript cannot infer the type of the looked up property correctly <sup>[[2]]</sup> when `record.lookup` is used in a `flow`. Nevertheless, I've reported [this issue](https://github.com/gcanti/fp-ts/issues/1816) in fp-ts.

Without using the `Option` monad, the example code may look like this:

```ts
interface LookupCount {
  (_: Record<string, number>): { count: number } | null;
}

const lookupCount: LookupCount = (r) => {
  if (r["total"]) {
    return { count: r["total"] };
  } else {
    return null;
  }
};

export const res = lookupCount({ total: 42 });
// Value of res is { count: 42 };

export const res2 = lookupCount({});
// Value of res2 is null
// Type of both res and res2 is { count: 42 } | null
```

Even though the implementation looks fairly neat, the disadvantage of this approach is incurred at the call site. Working with the result down the line requires us to check whether the value is `null` every single time until a fallback value is defined or an error is thrown. Using the `Option` monad, we can simply keep on mapping and chaining functions completely removing the need for null checks.

## Chaining

`option.chain` works exactly the same way as `either.chain` does.

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

const makeCount = (x: string, y: string) => (r: Record<string, number>) =>
  pipe(
    record.lookup(x, r),
    option.chain((a) =>
      pipe(
        record.lookup(y, r),
        option.map((b) => ({ count: a + b }))
      )
    )
  );

const countDogsAndCats = makeCount("dogs", "cats");

export const res = countDogsAndCats({ dogs: 21, cats: 21 });
// Value of res is { _tag: "Right", value: { count: 42 } }

export const res2 = countDogsAndCats({});
// Value of res2 is { _tag: "None" }
// Type of both res and res2 is Option<{ count: number }>
```

In the above example, the code performs two record lookups. Only if both lookups result in a hit, the values are added together and wrapped in an object of type `{ count: number }`. This implementation isn't perfect, though. Imagine we'd need to perform five lookups and additions in total. This approach would yield deeply nested code. We'll demonstrate how to improve this in the [bind](#bind) section.

Not using the `Option` monad, we arrive at a simpler implementation of `makeCount`. However, the approach suffers from the same disadvantages as the imperative code example under [mapping](#mapping).

```ts
const makeCount = (x: string, y: string) => (r: Record<string, number>) => {
  if (r[x] && r[y]) {
    // @ts-expect-error TS still thinks r[x] and r[y] may possibly be undefined
    return r[x] + r[y];
  }

  return null;
};

const countDogsAndCats = makeCount("dogs", "cats");

export const res = countDogsAndCats({ dogs: 21, cats: 21 });
// Value of res is { count: 42 }

export const res2 = countDogsAndCats({});
// Value of res2 is null
// Type of both res and res2 is { count: number } | null
```

## Other functions

Similarly to Either, the `Option` monad also implements other functions. In fact, it implements the same functions described in the [previous post]: `getOrElse`, `getOrElseW`, `fold`, `chainFirst`, `chainFirstW`, and some more (except for `getValidation`, which is Either-specific). These functions work the exact same way in every monad that implements them. Hence, there is no point in going through them again.

In the rest of this section, we'll demonstrate a new set of functions that are also implemented by `Either` and more or less all the monads explored in the rest of this series.

### bind

`bind` combines chain with an assignment to a _readonly_ property of a typed object. It returns a corresponding instance of `Option`.

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

const makeLookupCount = (x: string) => (r: Record<string, number>) =>
  pipe(
    option.some({}),
    option.bind("count", () => record.lookup(x, r))
  );

const lookupCount = makeLookupCount("total");

export const res = lookupCount({ total: 42 });
// Value of res is { _tag: "Some", value: { count: 42 } }

export const res2 = lookupCount({});
// Value of res2 is { _tag: "None" }
// Type of both res and res2 is Option<{ readonly count: number }>
```

### bindTo

`bindTo` creates a new typed object and assigns the `Some` value under a _readonly_ key of our choice. It also returns a corresponding instance of `Option`.

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

export const makeLookupCount = (x: string) => (r: Record<string, number>) =>
  pipe(record.lookup(x, r), option.bindTo("count"));
// makeLookupCount returns Option<{ readonly count: number }>
// after the full application of its parameters
```

With `bind` and `bindTo`, we can improve `makeCount`'s implementation from our earlier example for [chaining](#chaining):

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

export const makeCount =
  (x: string, y: string) => (r: Record<string, number>) =>
    pipe(
      record.lookup(x, r), // Option<number>
      option.bindTo("a"), // Option<{ a: number }>
      option.bind("b", () => record.lookup(y, r)), // Option<{ a: number; b: number }>
      option.map(({ a, b }) => ({ count: a + b })) // Option<{ count: number }>
    );
```

With this approach, we avoid deeply nested structures making our code more readable and maintainable.

### foldW

`foldW` is similar to `fold` from the [previous post]. The difference is that the `None` and `Some` handler functions no longer have to return the same type. This results in a widened return type:

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

export const res = pipe(
  option.some(42),
  option.foldW(
    () => ({ _tag: "Fallback" as const, value: 0 }),
    (value) => ({ _tag: "Derived" as const, value })
  )
);
// Value of res is { _tag: "Derived", value: 42 }
// Type of res is { _tag: "Derived", value: number } | { _tag: "Fallback", value: number }
```

### tryCatch

`tryCatch` creates a functional wrapper around functions performing a synchronous computation that can fail by throwing an exception. We can use this to our advantage when working with popular libraries or native functions written in a non-functional style.

```ts
import { option } from "fp-ts";
import * as fs from "fs";

export const res = option.tryCatch(() => fs.readFileSync("/path/to/your/file"));
// Type of res is Option<Buffer>
```

In the above example, if `readFileSync` throws an error - e.g. the file does not exist - the value of res will be an instance of `option.None`. This is a good solution if the missing file can be handled down the line. Should we want to keep the original error around, e.g. for reporting reasons, we may want to use `either.tryCatch` instead.

## Lifting

Now that we know two monads, we can delve into the concept of _lifting_. Up until now, all our examples were composing functions within the same monad. Oftentimes, we have to compose functions across different monads. In `fp-ts`, we can achieve this by "lifting one monad to another" by applying a function that takes a value of one type of a monad and returns a value of another:

![When lifting an Either to an Option, "Left" is mapped to "None" and "Right" is mapped to "Some"](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1sxzjyp6jdz2kimzxre6.jpg)

Assume we had a value of type `Either<Error, Record<string, number>>`, and we wanted to lookup the value of a key in case it was a `Right`. One option would be to extract that value and pipe it into `record.lookup`.

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

export const res = pipe(
  either.right({ total: 42 } as Record<string, number>),
  either.getOrElse(() => ({} as Record<string, number>)),
  record.lookup("total")
);
// Value of res is { _tag: "Some", value: 42 }
// Type of res is Option<number>
```

However, in that case, we would have to handle the `Left` type by specifying some kind of a fallback logic. We might not want to do that just yet. Therefor, the other option is to lift the `Either` to an `Option` and chain `record.lookup`:

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

export const res = pipe(
  either.right({ total: 42 } as Record<string, number>),
  option.fromEither,
  option.chain(record.lookup("total"))
);
// Value of res is { _tag: "Some", value: 42 }
// Type of res is Option<number>
```

This way, we don't need to handle the `Left` value ourselves since it is mapped to a corresponding `None` value by `option.fromEither`.

Even though the name of the concept may suggest so, lifting is not necessarily unidirectional. We can lift an `Option` to an `Either` almost as easily:

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

export const res = pipe(
  option.some(42), // Option<number>
  either.fromOption(() => "Missing value") // Either<string, number>
);
// Value of res is { _tag: "Right", value: 42 }
```

The only difference is that we have to pass in a function to specify how to lift the `None` value to a `Left<E>`. Remember, the None value does not hold any actual value, there's none. This is not the case with `Left`, which is a generic type.

## Wrap-up

- `Option` represents optionality or nullability.
  - `None` stands for a missing value.
  - `Some` stands for a present value.
- We use:
  - `bindTo`: to create a typed object and assign a given `Option`'s value to a specific key of that object. This returns an Option wrapping that typed object.
  - `bind`: to chain a computation and assign the resulting Option's value to a specific key of a typed object. It often follows a call to `bindTo` and solves the issue of deeply nested chaining.
  - `foldW`: to extract a single value from an Option while applying transformations to both the none and some values while widening the Some type.
  - `tryCatch`: to create a functional wrapper around synchronous computations that may fail by throwing an exception. It is usually used over functions coming from third-party libraries or native functions written in a non-functional style.
- `Lifting` helps us compose functions across different types of monads by taking one monad, such as Either, and transforming it into another, for example an `Option`.

In the next post, we’ll take a look at how to write asynchronous code that composes well using the `Task` monad.

## Extra resources

- [Does your Monad even Lift?]
- [All code samples in this post are available on Github]

[previous post]: https://dev.to/attila_vecerek/the-either-monad-3ooc
[1]: https://github.com/gcanti/fp-ts/blob/9da2137efb295b82fb59b7b0c2114f2ceb40a2b5/src/Option.ts#L110-L131
[tagged union]: https://en.wikipedia.org/wiki/Tagged_union
[2]: https://github.com/gcanti/fp-ts/issues/1527#issuecomment-877753020
[Does your Monad even Lift?]: https://mmhaskell.com/blog/2022/3/14/does-your-monad-even-lift
[All code samples in this post are available on Github]: https://github.com/vecerek/blog/tree/main/packages/fp-in-ts/src/04-the-option-monad