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;
}
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;
}
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!";
}
}
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!";
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(
(string: string) => /[a-zA-z]/.test(string),
(name) => `"${name}" is not a valid name!`
);
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:
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(
(name: string) => /[a-zA-z]/.test(name),
(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))
);
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(
(name: string) => regexLetters.test(string),
(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)
);
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(
(name: string) => regexLetters.test(regexLetters),
(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(makeError)
);
// (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!`));
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(
(name: string) => regexLetters.test(name),
(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(makenerror)
);
// (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!`);
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 (7)
Just for the record i was trying to implement it and got:
the solution:
Looks like it's not inferring the input type from the signature. I'll update this, thank you!
You define 'regexLetters' but forgot to use it
Will fix, thanks a lot!
👍 thanks Wayne. Appreciate it.
Overall thank you! For clarity's sake, should it be
E.mapLeft(makeError)
instead ofE.mapLeft(error)
?Thanks for that!
mapLeft
takes a function, which is why we providedmakeError
as the argument.error
does not exist unless you make exist by creating a variable from within a closure inmapLeft
.