DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Cover image for Either: fp-ts
Wayne Van Son
Wayne Van Son

Posted on • Updated on

Either: fp-ts

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 separated 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"))
  .toStrictEqual({ name: "Wayne" });

expect(main("168"))
  .toBe(`"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 implementing 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 separated or inline.
But when you need to use the same code more than twice in a code base, the separated functional approach is what you may lean towards.

Top comments (5)

Collapse
nickwireless profile image
Nick Hanigan

πŸ‘ thanks Wayne. Appreciate it.

Collapse
josuevalrob profile image
Josue Valenica

Just for the record i was trying to implement it and got:

TypeError: Method RegExp.prototype.test called on incompatible receiver undefined

the solution:

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

You define 'regexLetters' but forgot to use it

Collapse
ziggyzag profile image
Ziggy

Overall thank you! For clarity's sake, should it be E.mapLeft(makeError) instead of E.mapLeft(error)?

Collapse
waynevanson profile image
Wayne Van Son Author

Thanks for that!

mapLeft takes a function, which is why we provided makeError as the argument.

error does not exist unless you make exist by creating a variable from within a closure in mapLeft.

🌚 Life is too short to browse without dark mode