DEV Community

loading...
Cover image for Either: fp-ts

Either: fp-ts

Wayne Van Son
fp-ts, typescript, haskell, purescript and linux user with an endeavor to explore the world of business with functional analysis
・5 min read

Photo by Toby Elliott on Unsplash

Introduction

I received an email from an Aussie admirer of my last post asking for part two of the series.
Deja Vu, here we are again with another fp-ts data structure for handling conditional logic.

We've already explored the data structure Option<A> as a functional replacement to handle if statements.

Today we'll gear our minds towards handling what happens when if needs an else.

Signature

type Either<E, A> = Left<E> | Right<A>;

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

interface Left<E> {
  _tag: "Left";
  value: E;
}
Enter fullscreen mode Exit fullscreen mode

Left and Right can be distinguished with the _tag property on the object, which is available at runtime. It's leveraged by the functions within the module to map over.

replace if/else with Either

Our goal will be to create a Person struct, where the constraint is that a name must be letters only, no spaces, no numbers.

If it does not match this, we need options to handle it (functionally).

interface Person {
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

Using the language

function validateName(name: string): string {
  // regex for letters only
  if (/[a-zA-z]/.test(name)) {
    return name;
  } else {
    return "not a valid name!";
  }
}
Enter fullscreen mode Exit fullscreen mode

We can use the ternary operator to make this a lot smaller, but the logic is the same.

const validateName = (name: string): string =>
  /[a-zA-z]/.test(name) ? name : "not a valid name!";
Enter fullscreen mode Exit fullscreen mode

Did you notice the return value is string, regardless of whether it is valid or not?

If we wanted to differentiate the return value with the same type (string, number, etc), we must put it in a box/data structure.

Using fp-ts

Let's use the Either data structure and see how it looks.

Constructors

import { either as E } from "fp-ts";

// (name: string) => Either<string, string>
const validateName = E.fromPredicate(
  /[a-zA-z]/.test,
  (name) => `"${name}" is not a valid name!`
);
Enter fullscreen mode Exit fullscreen mode

fromPredicate is a function derived from the MonadThrow typeclass.

It is a constructor, meaning it can create the data structure. In this case it creates an Either using a predicate function.

Combinators

Now because we're using a data structure with the familiar fp-ts API, we have access to all other combinators applicable to this structure.

These can be found here:

https://github.com/gcanti/fp-ts/blob/9ff0cb6a7264c03254ff60232fb44dba3841a340/src/Either.ts#L1262-L1290

We'll use the functions map and mapLeft, derived from the MonadThrow typeclass.

// example of inline function composition
import { either as E } from "fp-ts";
import { flow } from "fp-ts/function";

interface Person {
  name: string;
}

// (name: string) => Either<string, Person>
const makeUser = flow(
  E.fromPredicate(
    /[a-zA-z]/.test, 
    (name) => `"${name}" is not a valid name!`
  ),
  // applies the function over `Right`, if it is `Right`
  E.map((name): Person => ({ name })),
  // applies the function over `Left`, if it is `Left`
  E.mapLeft((message) => new Error(message))
);
Enter fullscreen mode Exit fullscreen mode

Nothing stops us from composing our functions inline as demonstrated.

Since we're using functions, let's split out some inline functions. We do this when we need to use them elsewhere in our hypothetical code base or if it's easier for you to read.

// example of seperated functional composition
import { either as E } from "fp-ts";
import { flow } from "fp-ts/function";

interface Person {
  name: string;
}

const regexLetters = /[a-zA-z]/;

// (name: string) => Either<string, string>
const validateName = E.fromPredicate(
  /[a-zA-z]/.test,
  (name) => `"${name}" is not a valid name!`
);

const makeError = (message: string) => new Error(message);

// (name: string) => Either<Error, Person>
const makeUser = flow(
  validateName,
  E.map((name): Person => ({ name })),
  E.mapLeft(error)
);
Enter fullscreen mode Exit fullscreen mode

Beautiful. Now we can use these functions where ever we may. The most useful I think is regexLetters and makeError

Destructors

Well there are a few destructors available, so we'll use the fold and getOrElse functions.

fold takes two functions, where the first is a case for Left and the second is a case for Right.

// using fold
import { either as E } from "fp-ts";
import { flow } from "fp-ts/function";

interface Person {
  name: string;
}

const regexLetters = /[a-zA-z]/;

const validateName = E.fromPredicate(
  /[a-zA-z]/.test,
  (name) => `"${name}" is not a valid name!`
);

const makeError = (message: string) => new Error(message);

const makeUser = flow(
  validateName,
  E.map((name): Person => ({ name })),
  E.mapLeft(error)
);

// (name: string) => string
const main = flow(
  makeUser,
  E.fold(
    (error) => error.message,
    ({ name }) => `Hi, my name is "${name}"`
  )
);

expect(main("Wayne"))
  .toMatchObject(E.right(`Hi, my name is "Wayne"`));

expect(main("168"))
  .toMatchObject(E.left(`"168" is not a valid name!`));
Enter fullscreen mode Exit fullscreen mode

An alternative option is to use getOrElse, which we can use if we don't need to change the output of the Right value in Either

// using getOrElse
import { either as E } from "fp-ts";
import { flow } from "fp-ts/function";

interface Person {
  name: string;
}

const regexLetters = /[a-zA-z]/;

// (name: string) => Either<string, string>
const validateName = E.fromPredicate(
  /[a-zA-z]/.test,
  (name) => `"${name}" is not a valid name!`
);

const makeError = (message: string) => new Error(message);

const makeUser = flow(
  validateName,
  E.map((name): Person => ({ name })),
  E.mapLeft(error)
);

// (name: string) => string | Person
const main = flow(
  makeUser,
  // `W` loosens the constraining result type,
  // otherwise it would force us to make it `Person` type.
  E.getOrElseW((error) => error.message)
);


expect(main("Wayne"))
  .toMatchObject(E.right({ name: "Wayne" }));

expect(main("168"))
  .toMatchObject(E.left(`"168" is not a valid name!`));
Enter fullscreen mode Exit fullscreen mode

There a few more, but these are the most common and I rarely use the rest.

Recommended Reading

The next step is tying all your error handling by implenting gcanti's guide to Either and Validation.

Notes

I find using fp-ts scales a project way better, where enforcing the constraints of "pure" functional programming really makes a difference when practicing domain driven development.

Keep in mind it's not worth folding/destructing your data structure until you have to. Usually there is a main function that is the entry point into an application and this where most of my folding happens.

It's up to your taste if you like the functions seperated or inline.
But when you need to use the same code more than twice in a code base, the seperated functional approach is what you may lean towards.

Discussion (1)

Collapse
nickwireless profile image
Nick Hanigan

👍 thanks Wayne. Appreciate it.