DEV Community

Attila Večerek
Attila Večerek

Posted on • Updated on

The Either monad

The Either monad specifies the Either data type as well as several functions that operate on top of it. The Either data type represents the result of a computation that may fail. This is how fp-ts defines it [1]:

interface Left<E> {
  readonly _tag: "Left";
  readonly left: E;
}

interface Right<A> {
  readonly _tag: "Right";
  readonly right: A;
}

export type Either<E, A> = Left<E> | Right<A>;
Enter fullscreen mode Exit fullscreen mode

As we can see, it is a simple tagged union with two distinct tags: Left and Right. Left represents the failure while right represents the success of a computation. A computation may fail for different reasons. In such cases, we can use another tagged union to represent the left type, e.g.:

// parse-int.ts
import type { either } from "fp-ts";

interface NaNError {
  readonly _tag: "NaNError";
  readonly value: number;
}

interface OutOfRangeError {
  readonly _tag: "OutOfRangeError";
  readonly value: number;
}

export type ParseIntError = NaNError | OutOfRangeError;

export type ParseIntResult = either.Either<ParseIntError, number>;

export type ParseInt = (x: string) => ParseIntResult;
export declare const parseInt: ParseInt;
Enter fullscreen mode Exit fullscreen mode

In the above example, we just created a new type signature for the (not so?) well known parseInt function. The function signature of the original parseInt function is the following:

(string: string, radix?: number | undefined) => number
Enter fullscreen mode Exit fullscreen mode

It returns NaN in case it cannot parse the string as an integer. It returns Infinity in case the parsed number is outside of the safe range, i.e. too big or too small [2]. The function signature does not indicate these edge cases because both NaN and Infinity are still typed as number. They can only be distinguished from valid numbers at runtime using the isNaN and isFinite functions, respectively.

Let's be honest, how many times do we actually remember to handle such edge cases? parseInt is not the only example of such functions. There are many that may fail but their type signature does not indicate that: JSON.parse, new Date(), new Array(), BigInt, etc.

In my opinion, encoding all possible outcomes of a computation in the type signature is generally better. Life is too short to read possibly nonexistent documentation. Also, there are more important things to keep in mind when our IDEs could help us make sure that these edge cases are handled. For example, by enabling the noImplicitReturns compiler option:

import { ParseIntError } from "./parse-int.js";

// @ts-expect-error TS2366: Function lacks ending return statement and return type does not include 'undefined'.
export const handleParseIntError = (err: ParseIntError): number => {
  if (err._tag === "NaNError") {
    return 0;
  }
};
Enter fullscreen mode Exit fullscreen mode

Or, by implementing and using an assertUnreachable function.

Now that we know why Either is useful, let's see how to actually use it.

Constructors

In the previous post, we learnt that every monad needs to be constructible, mappable, and chainable. The Either monad has two constructors, one for each tag:

import { either } from "fp-ts";

export const success = either.right(42);
export const failure = either.left({ _tag: "NaNError", value: NaN });
Enter fullscreen mode Exit fullscreen mode

Mapping

The Either monad offers two map functions: map and mapLeft. Each maps the encapsulated value of the corresponding tag. We may map the right/left values to a new value of the same type, or a completely different one, e.g.: turning Either<Error, number> into Either<Error, { count: number }> or Either<string, number>.

For the following code examples, assume we have implemented the parseInt function. We can then use map/mapLeft to evolve the success and failure paths, respectively:

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const parseCount = flow(
  parseInt,
  either.map((count) => ({ count })),
  either.mapLeft(({ value }) => `"${value}" could not be parsed as an integer`)
);

export const res = parseCount("42");
// Value of res is { _tag: "Right", right: 42 }

export const res2 = parseCount("abc");
// Value of res2 is { _tag: "Left", left: '"42" could not be parsed as an integer' }
// Type of both res and res2 is Either<string, { count: number }>
Enter fullscreen mode Exit fullscreen mode

Without using the Either monad (assume parseInt returns ParseIntError | number instead), the above code may look like this:

import { ParseIntError } from "./parse-int.js";

declare const parseInt: (_: string) => ParseIntError | number;

const parseCount = (x: string) => {
  const parsedInt = parseInt(x);

  if (typeof parsedInt === "number") {
    return { count: parsedInt };
  } else {
    return `"${parsedInt.value} could not be parsed as an integer`;
  }
};

export const res = parseCount("42");
// Value of res is 42

export const res2 = parseCount("abc");
// Value of res2 is '"42" could not be parsed as an integer'
// Type of both res and res2 is number | string
Enter fullscreen mode Exit fullscreen mode

This code follows more of an imperative programming paradigm. It requires us to:

  • store the intermediate result in a variable,
  • figure out how to name that variable,
  • fork the logic based on the return type.

With the Either monad code, we only need to declare what the different results should map to.

Chaining

chain is kind of similar to map. The difference is that the function passed into chain must return another Either. We can think of it as performing a mapping that may fail.

import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const add = (x: string, y: string) =>
  pipe(
    parseInt(x),
    either.chain(
      (a) =>
        pipe(
          parseInt(y),
          either.map((b) => a + b)
        ) // `Either<ParseIntError, number>` is returned by this pipe
    )
  );

export const res = add("42", "24");
// Value of res is { _tag: "Right", right: 66 }

export const res2 = add("abc", "24");
// Value of res2 is { _tag: "Left", left: { _tag: "NaNError", value: NaN } }
// Type of both res and res2 is Either<ParseIntError, number>
Enter fullscreen mode Exit fullscreen mode

In the above code, we first parse "42" as an integer. Then, we parse "24" as an integer but only if the first call to parseInt succeeds. Finally, if the second call to parseInt succeeds, we add the results together. Should any of the parsing steps fail, the corresponding left result is returned.

Without using the Either monad, the above code may look like this:

import { ParseIntError } from "./parse-int.js";

declare const parseInt: (_: string) => ParseIntError | number;

const add = (x: string, y: string) => {
  const parsedIntX = parseInt(x);
  const parsedIntY = parseInt(y);

  if (typeof parsedIntX === "number") {
    if (typeof parsedIntY === "number") {
      return parsedIntX + parsedIntY;
    } else {
      return parsedIntY;
    }
  } else {
    return parsedIntX;
  }
};

export const res = add("42", "24");
// Value of res is 66

export const res2 = add("abc", "24");
// Value of res2 is { _tag: "NaNError", value: NaN }
// Type of both res and res2 is number | ParseIntError
Enter fullscreen mode Exit fullscreen mode

This code shares the same properties with the imperative code example in the Mapping section. On top of that, it is less readable and maintainable because of the nested if statements. Imagine we added three more computations (not necessarily the same parseInt steps) each of which could fail. The level of nesting would be three levels deeper making it more likely to introduce bugs. chain allows us to flatten otherwise nested if statements like so:

import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

// either.right represents an arbitrary computation that may fail in this example

pipe(
  parseInt("42"),
  either.chain((x) => either.right(x)),
  either.chain((x) => either.right(x)),
  either.chain((x) => either.right(x)),
  either.chain((x) => either.right(x))
);
Enter fullscreen mode Exit fullscreen mode

Other functions

Besides constructors, mappers, and chainers, monads may implement any additional interfaces. This section describes some of the other functions implemented by the Either monad.

getOrElse

The map and mapLeft functions, as demonstrated in the Mapping section, produce an instance of an Either. What if we wanted to extract its value? That's when we make use of getOrElse, which forces us to define a fallback value in case of a failure.

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const myParseInt = flow(
  parseInt,
  either.getOrElse(() => 0)
);

export const res = myParseInt("42");
// Value of res is 42, not a Right

export const res2 = myParseInt("abc");
// Value of res2 is 0, not a Left
// Type of both res and res2 is number
Enter fullscreen mode Exit fullscreen mode

We can use getOrElseW if we want to widen - that's what the W at the end of the function name stands for - the right type of the Either:

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const myParseInt = flow(
  parseInt,
  either.getOrElseW(() => "Zero" as const)
);

export const res = myParseInt("42");
// Value of res is 42

export const res2 = myParseInt("abc");
// Value of res2 is "Zero"
// Type of both res and res2 is number | "Zero"
Enter fullscreen mode Exit fullscreen mode

In the unlikely scenario that the failure is unrecoverable and we want our program to crash, we could also use getOrElseW to throw an error:

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const throwError = (e: Error) => {
  throw e;
};

const myParseInt = flow(
  parseInt,
  either.getOrElseW(flow(either.toError, throwError))
);

export const res = myParseInt("42");
// Value of res is 42
// Type of res is number

export const res2 = myParseInt("abc");
// res2 never gets assigned a value because an error is thrown
Enter fullscreen mode Exit fullscreen mode

either.toError takes any value and does a best effort conversion to an Error object. throwError takes an Error object and throws it. Functions that don't return anything and just throw, have a return type of never. Hence, getOrElseW in this example widens the type of number by never. However, number | never is the same type as number.

fold

fold is a function that allows us to transform both tags of an Either at the same time. It expects two functions as the first set of arguments:

  1. A function that takes a value of the left type of the Either and returns a value of type A (it can be anything).
  2. A function that takes a value of the right type of the Either and also returns a value of type A.

With an Either value provided as the second argument, fold executes one of the two provided functions based on the tag and returns a value of type A. It can be used to extract the encapsulated value of the Either as well as perform mappings at the same time:

import { either } from "fp-ts";
import { flow, pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

const throwError = (e: Error) => {
  throw e;
};

export const res = pipe(
  parseInt("42"),
  either.fold(flow(either.toError, throwError), (count) => ({ count }))
);

// Type of res is { count: number }

// Functionally the same as writing the below code
// but using fewer steps (1 instead of 3):

export const res2 = pipe(
  parseInt("42"),
  either.mapLeft(either.toError),
  either.map((count) => ({ count })),
  either.getOrElseW(throwError)
);
Enter fullscreen mode Exit fullscreen mode

It is still possible to throw an error using our throwError function from the previous example because any value is assignable to the type of never.

chainFirst

chainFirst is similar to chain. It takes a function that has to return an Either. If that function returns a Right value, its result is thrown away. In such case, the result of the previous computation is returned, i.e. the argument passed into the provided function. In the opposite case, the Left value is returned. I tend to use it for logging intermediate values or setting breakpoints.

import { either } from "fp-ts";
import { pipe } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

export const res = pipe(
  parseInt("42"),
  either.chainFirst((count) => {
    console.log({ count });

    return either.right(undefined);
  }),
  either.getOrElse(() => 0)
);

// Type of res is number
Enter fullscreen mode Exit fullscreen mode

There's also a chainFirstW alternative, which may be useful for fail-fast type of validations that keep widening the Left type with new failure types:

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function.js";
import { parseInt } from "./parse-int.js";

interface IncorrectAnswerError {
  readonly _tag: "IncorrectAnswerError";
  readonly value: number;
}

const myParseInt = flow(
  parseInt,
  either.chainFirstW((count) =>
    count === 42
      ? either.right(undefined)
      : either.left({
          _tag: "IncorrectAnswerError",
          value: count,
        } as IncorrectAnswerError)
  )
);

export const res = myParseInt("42");
// Value of res is { _tag: "Right", right: 42 }

export const res2 = myParseInt("abc");
// Value of res2 is { _tag: "Left", left: { _tag: "NaNError", value: NaN } }

export const res3 = myParseInt("41");
// Value of res3 is { _tag: "Left", left: { _tag: "IncorrectAnswerError", value: 41 } }
// Type of res, res2, res3 is the same: Either<ParseIntError | IncorrectAnswerError, number>
Enter fullscreen mode Exit fullscreen mode

Assume we were to validate a form with multiple input fields and we wanted to present all the errors at once. We'd have to somehow collect multiple possible failures instead of returning on the first encountered one. In such cases, we should use either.getValidation. There's already a great article written about it by the author of fp-ts himself. I highly recommend checking it out.

Wrap-up

  • Either represents the result of a computation that may fail.
    • Left stands for failure.
    • Right stands for success.
  • We use:
    • map: to transform the right value into a different value and this transformation cannot fail.
    • mapLeft: to transform the left value into a different value and this transformation cannot fail.
    • getOrElse: to extract the right value while setting a default value in case of a Left.
    • getOrElseW: to extract the right value while widening its type, or throwing an error on an unrecoverable Left.
    • fold: to extract a single value from Either while applying transformations to both the left and right values.
    • chain: to transform the right value into a different value and this transformation can fail.
    • chainFirst: to log intermediate values or set breakpoints for debugging.
    • chainFirstW: to perform one or more fail-fast type of validations.
    • getValidation: to perform validations that can be collected.

If I missed a function from the Either monad that you often use or find particularly interesting, or I didn't cover a use case for the functions described in this post, please let me know in the comments 🙏

In the next post of this series, we will look into the Option monad.

Extra resources

Latest comments (2)

Collapse
 
amite profile image
Amit Erandole • Edited

This code does not compile as you show it to.

import { either } from "fp-ts";
import { flow } from "fp-ts/lib/function";
import { parseInt } from "./parse-int";

const parseCount = flow(
  parseInt,
  either.map(count => ({ count })),
  either.mapLeft({ value } => `"${value}" could not be parsed as an integer`)
);

const res = parseCount("42");
// Value of res is { _tag: "Right", right: 42 }

const res2 = parseCount("abc");
Enter fullscreen mode Exit fullscreen mode

I get the error:

No overload matches this call.
  The last overload gave the following error.
    Argument of type '<E>(fa: Either<E, unknown>) => Either<E, { count: unknown; }>' is not assignable to parameter of type '(b: number) => Either<unknown, { count: unknown; }>'.
      Types of parameters 'fa' and 'b' are incompatible.
        Type 'number' is not assignable to type 'Either<unknown, unknown>'
Enter fullscreen mode Exit fullscreen mode
Collapse
 
attila_vecerek profile image
Attila Večerek

Hi @amite, thanks for reporting this issue. I forgot the parentheses around the destructuring operator. The following line:

either.mapLeft({ value } => `"${value}" could not be parsed as an integer`)
Enter fullscreen mode Exit fullscreen mode

should be:

either.mapLeft(({ value }) => `"${value}" could not be parsed as an integer`)
Enter fullscreen mode Exit fullscreen mode

I'm trying to test all code examples in these articles but sometimes I forget. I'll make sure to fix this in the post, too.