Photo by Tom Morel on Unsplash, show him some love!
Introduction
I've fallen in love with fp-ts
and the ecosystem. Once the main concepts are grasped and the code starts to flow, life is bliss. I will be very sad when my day job requires imperative style programming.
It solves many problems I've had coding, but I'll write about that another day.
One issue with the fp-ts
ecosystem is that the docs are not designed for beginners; It's assumed you know the jargon and theory of functional programming.
The goal of this piece is to provide the information I was missing on my journey to aid those that may need it. If it's helpful please let me know somehow: like the post, otherwise I'm accessible via email or twitter.
We'll describe how to switch from using the built in if
, else
and switch
keywords and using the fp-ts
equivalents: Option
and Either
monads.
Swap if
for our saviour, Option<A>
Logic gates are a core of programming. More often than not, we're trying to assert if it IS something rather than isn't. If you're not a believer, work with io-ts
for a week and you too can feel the joy of non-defensive programming.
Our current options for validation are:
-
throw new Error()
if the validation fails. - Return
null
if the validation fails.
We will replace these with Option<A>
, which represents null as None
and the non-null value as Some<A>
.
Don't throw new Error()
Here our function of lovely ensures that our string a
starts with the word "lovely"
, otherwise it throws an Error
.
function startsWithLovely(a: string): string {
if(!a.startsWith("lovely")) {
throw new Error()
}
return a
}
expect(startsWithLovely("lovely to meet you"))
.toBe("lovely to meet you")
expect(startsWithLovely("nice to meet you"))
.toThrowError()
The program will stop completely, with side effects you do or don't know about still running from earlier with null values (requests, responses).
If throw errors to catch them, we'll demonstrate some pure patterns in part 2/3.
Instead, let's try the next "best thing": null
.
Returning null
Here our function of lovely ensures that our string a
starts with the word "lovely"
, otherwise it return null
.
function startsWithLovely(a: string): string | null {
if(!a.startsWith("lovely")) {
return null
}
return a
}
expect(startsWithLovely("lovely to meet you"))
.toBe("Lovely to meet you")
expect(startsWithLovely("nice to meet you"))
.toBe(null)
Our startsWithLovely
function is now pure (nothing outside the function is changed).
The problem now is that we have to check that the value returned is not null
whenever we want to use it. How annoying!
Our saviour, Option<A>
In functional programming, null
is not used. The short story is that it was a mistake that gets worse in Javascript, because typeof null === "object"
.
It's not obvious in these examples because we're working with primitives like string
and boolean
. Once we start mixing null
and Javascript objects together, it'll feel like running with arthritic knees.
The Option<A>
is one of the most common types (boxes) you'll use.
import { option as O } from "fp-ts"
function oStartsWithLovely(a: string): O.Option<string> {
if(!a.startsWith("lovely")) {
return O.some(a)
}
return O.none
}
An improvement on this would be to use ternary operators, but instead we can use the constructor O.fromPredicate
.
import { option as O } from "fp-ts"
function oStartsWithLovely(a: string): O.Option<string> {
return O.fromPredicate(s => s.startsWith("lovely"))(a)
}
Or even one better:
import { pipe } from "fp-ts/lib/function"
import { option as O } from "fp-ts"
const startsWithLovely = (a: string): boolean =>
a.startsWith("lovely")
const oStartsWithLovely = O.fromPredicate(startsWithLovely)
Note that oStartsWithLovely
has the signature (a: string) => O.Option<string>
across all three examples above.
Composition with Option
Now we know when to use Option
, let's look at how we can use it.
Our goal is to validate a string. It must meet all these conditions:
- start with "lovely".
- contain "meet".
- end with "you".
The way we'd usually do this is with if
and returning null, only returning the input value if it passes all the checks.
const example = (a: string): string | null => {
const conditions =
a.startsWith("lovely") ||
a.endsWith("you") ||
a.includes("meet")
if (conditions) {
return a
}
return null
}
This might look normal to you. It did for me. Since we can control our own destiny, let's look at a few ways to use Option
.
Chain
Chain works analogously to Array.map
then Array.flatten
, but instead of an Array<A>
we'll use Option<A>
.
import { pipe } from "fp-ts/lib/function"
import { option as O } from "fp-ts"
const startsWithLovely = (a: string) =>
a.startsWith("lovely")
const endsWithYou = (a: string) =>
a.endsWith("you")
const containsMeet = (a: string) =>
a.includes("meet")
const oExample = (a: string | null) =>
pipe(
a,
O.fromNullable
O.chain(O.fromPredicate(startsWithLovely)),
O.chain(O.fromPredicate(endsWithYou)),
O.chain(O.fromPredicate(containsMeet)),
)
This is a lot better, but you see the pattern right? We're using O.chain(O.fromPredicate(condition)))
over and over.
What if had to have 20 validation checks? copy-pasta should not the solution.
Let's refactor this with what might feel like pure magic.
import { pipe } from "fp-ts/lib/function"
import {
option as O,
array as A,
} from "fp-ts"
const startsWithLovely = (a: string) =>
a.startsWith("lovely")
const endsWithYou = (a: string) =>
a.endsWith("you")
const containsMeet = (a: string) =>
a.includes("meet")
const conditions = [
startsWithLovely,
endsWithYou,
containsMeet
]
const oExample = (a: string) =>
pipe(
conditions,
// conjunction: all must be `Some`
A.reduce(O.some(a), (oPrev, condition) =>
pipe(
oPrev,
O.chain(O.fromPredicate(condition))
)
)
)
This is putting all condition checkers in an array and iterating over each one, returning the results.
If you're coming from a non-functional background, this is a much better example than using and A.foldMap(monoidAll)(condition => condition(a))(conditions)
.
Parting words
This might seem like a jump in complexity, and it is to some extent. For simple examples it can be overkill, but what about more complex examples? The demand to create increasingly complex applications requires smarter, higher level abstractions. Functional programming fits abstraction neatly at a low level: code.
Programmers usually sit around the 85% percentile, so it's fair to say that we have the intellect to handle the abstraction. Some really smart people have conjured up the beauty of category theory, so let's follow in their footsteps.
If this helpful, I encourage you to utilize the functional paradigm and make the "boring" jobs fun again!
Part two will be on it's way soon.
Top comments (5)
Hi Wayne, thanks for your post. I have more pragmatic view on such things like fp-ts. I have even forced it in one of projects what currently I consider as mistake. The thing is that you deliberetly don't compare your code with idiomatic examples with ifs and switches. And in my opinion such comparission would look bad for your fp-ts version which is just overcomplicated.
I see that the code is neat and uses some nice constructs, but still nobody will say it's more readable that few simple conditions. And conditions are nothing wrong, FP also is about making branching, and this is why all FP Lang's have pattern matching.
Also FP is not about typeclasses or high opaque types. You can make FP and use null. It has no such properties like Maybe, but with new operators like ?? and ?. It's not so bad. First of all in TS you see what can or can't be null and this is big value on its own.
I don't think it is right. Maybe implements many typeclasses but not is one. It is type with two values.
I don't fall anymore in high abstraction so often, I learned after my mistakes.
My full point about using Nullable vs Maybe you can find here - maybe just Nullable
I haven't used any other idomatic keywords like else or switch because I'm saving those for the next article when we explore Either.
As mentioned, it seems like over and is not simpler compared to using just the keywords.
The aim of the article is to show how we'd approach converting simple idiomatic JavaScript into using fp-ts.
We can use null in FP by how you may define it. I've defined it as using higher minded types like in Haskell and F#. HKT's allow composition, which is the most important element of a pleasurable functional programming experience.
If we follow the pattern of using HKT's, we get fantastic solutions for side effects and promises with Task and IO types due to their laziness.
We'll never have to await for promises again!
You're right, Option is a type, not a typeclass. I'll change that now.
We still need to write code, which is inherently low level. The abstraction is for something like DDD. Lifting a little bit at low level using HKT's is great because we can define more complex models for our domain with these abstractions.
I read your article and took that into consideration when responding.
I see your fascination for HKT.
HKT is not about composition but about code reuse and pattern reuse. It allows for very high polimorhism. F# has no HKT.
Again you can make these abstractions without HKT. For me FP is great but I really don't need HKT in a language which doesn't support it. HKT fits great where it is idiomatic so in Haskell or PureScript. TypeScript not support HKT, and what fp-ts is doing is a hack.
I feel like pattern reuse is composition, which means that HKT is compositional by nature.
How would you define composition?
That's correct, sorry about that. I don't know F# very well, but it looks like it has HKT because it's got the
Async<T>
type and it has methods, just like a typeclass, to manipulate code in async world. Thanks for mentioning this.We can, such as with fluture: lazy promises.
The "hack" is what makes it special. If it wasn't we'd just use purescript.
I feel like more people move to fp-ts than to purescript because being close to the javascript specification is important.
Maybe because it's familiar, maybe because it drives the internet, maybe because it integrates well into existing codebases.
People and companies are seeing use cases which require these compositional aids. Grammarly is just one of them, which they did a talk on describing their use case.
To anyone listening, part two is out! Scroll to the top and it's will be next in the series.