Monads for the Dysfunctional
Since React took off, Functional Programming has really taken off and is being used in more places. Not one to get left behind I tried learning it and applying it in as many places as I could 5 or 6 years ago but quickly realized it wasn’t for me. I’m not going to get into all the reasons for that since most of them are just preference but the big reason why I keep my usage of Functional concepts to somewhat of a minimum is that it makes things overly complicated in many cases.
For example, one of the foundational concepts in Functional Programming
is the Monad
structure. To understand it (at least to understand it
well) you need to learn a whole bunch of related concepts none of which
are plainly laid out for newbies. However the use of Monads can really
make your code easier to understand in many cases. To that end I’m
writing up a super simplified, no frills description of what a Monad
is, why they’re useful, and some examples of them.
What is a monad?⌗
According to the massive page on Monads in the Haskell docs, a Monad is a structure (they make it clear it doesn’t have to be a data structure) which has something akin to the following interface (note that this is in Typescript parlance which is slightly different).
interface Monad<T> {
constructor(value: T): Monad<T>;
bind<ReturnType>(callable: (value: T) => Monad<ReturnType>) => Monad<ReturnType>;
}
So basically there’s three parts to the interface:
- Some generic type
T
which is the data type the Monad works with (or sometimes contains) - A constructor (or a static method
of
) which takes a value of typeT
and returns aMonad<T>
- A
bind
method which can be used to apply some function to the data represented by the Monad
On the surface this doesn’t seem all that interesting but let’s look at a few examples and see if we can make things clearer of why this interface is useful.
Examples⌗
Promises⌗
One really good example from the JavaScript/Browser API is the Promise type.
The Promise
type provides 3 ways to create a new instance:
new Promise(callback)
, Promise.resolve(value)
, and Promise.reject(value)
.
This fulfills the first part of the interface.
It also has the following methods: .then(callback)
, .catch(callback)
,
and .finally(callback)
which are equivalent to the .bind
method
but with specialized usage.
So now that we’re pretty sure that a Promise
is a Monad, let’s look
at why it’s useful.
Without a Promise
, if you wanted to perform some asynchronous
operation you would need to do something like this:
function remoteOperation(callback: (value: string) => void): void {
// snip...
}
function anotherRemoteOperation(callback: (value: SomeType) => void): void {
// snip...
}
remoteOperation((value) => {
anotherRemoteOperation((anotherValue) => {
// and so on...
})
});
With Promise
we can rewrite this as:
function remoteOperation(): Promise<string> {
// snip...
}
function anotherRemoteOperation(): Promise<SomeType> {
// snip...
}
remoteOperation.then(anotherRemoteOperation)
It’s much easier to chain a Promise
than it is to keep doing
callbacks.
Another nice feature of this particular Monad is that the functions
that are given in the .then
, .catch
, and .finally
methods don’t
do anything until the Promise
actually resolves which makes the
whole thing lazy in a good way. And if the Promise
never resolves
(this might be a good thing) then the cost of running those functions
is never incurred. This leads us into the next useful Monad.
Maybe/Optional⌗
A Maybe
(or sometimes called Optional
) is a special type that you
can think of as an array that con only zero or one element in it any
any time. Again this seems silly but let’s look at a super trivial
example.
function tryGetName(): undefined | string {
// snip trying to get the name, return `undefined` otherwise
}
const maybeName = tryGetName();
if (maybeName === undefined) {
console.log("Hello, anonymous user!");
} else {
console.log(`Hello, ${maybeName}!`);
}
Now sure, this could be condensed down in a couple ways but the logic would still be following this pattern and it’s a bit clearer to read it this way.
Thanks to Typescript we can be pretty confident we handled all the
possible values of maybeName
(in other languages you may not have
type checking which would make this riskier). However we’re having to
put the fact that the variable might have multiple types of values
front and center which can be a bit annoying the more frequently the
value is used.
Let’s look at an alternative way this could be written.
function tryGetName(): Maybe<string> {
// snip trying to get the name
}
maybeName
.map((name: string) => `Hello, ${name}!`)
.orElse(() => "Hello, anonymous user!")
.bind(console.log);
It’s basically the same number of lines of code but at no point can
you accidentally dereference an undefined
value and there are no
visible branches in the code which can be useful.
It’s also not very hard to write this particular type.
class Maybe<T> {
public static of<T>(value: T): Maybe<T> {
return new Maybe(value);
}
public static none<T>(): Maybe<T> {
return new Maybe(undefined);
}
private readonly maybeValue: undefined | T;
private constructor(value: undefined | T) {
this.maybeValue = value;
}
public map<ReturnType>(transform: (value: T) => ReturnType): Maybe<ReturnType> {
if (this.maybeValue === undefined) {
return Maybe.none();
}
return Maybe.of(transform(this.maybeValue));
}
public flatMap<ReturnType>(transform: (value: T) => Maybe<ReturnType>): Maybe<ReturnType> {
if (this.maybeValue === undefined) {
return Maybe.none();
}
return transform(this.maybeValue);
}
public orElse(otherValue: T): Maybe<T> {
if (this.maybeValue !== undefined) {
return this;
}
return Maybe.of(otherValue);
}
public bind(operation: (value: T) => void): void {
if (this.maybeValue === undefined) {
return;
}
operation(this.maybeValue);
}
public unwrap(): undefined | T {
return this.maybeValue;
}
}
It’s a very simple type but it allows for easy chaining of operations
without having to continuously check for potentially undefined
values. It’s also hopefully pretty clear how it’s similar to the
Promise
type.
Result⌗
A slight variation on the Maybe
type is the result type. This type
represents the result of some operation. Just like with Maybe
it
only holds a single value but the type of that value is dependent on
what the result was of the operation it came from. For example, maybe
you are creating some remote resource. If the creation succeeded,
Result
would contain the newly created resource, otherwise it would
contain some error information about what went wrong.
This can prevent users of your API from being able to ignore errors
while doing development which is super easy to do in many languages
which allow for “throwing” errors. If you don’t wrap the operation in
a try/catch block then the error could be ignored until it happens at
runtime which isn’t great. With a Result
type it’s clear you must
handle the possibility of failure before using the success value.
Here’s an example of how such a type could be used:
interface User {
id: string;
username: string;
}
type UserCreationRequest = Omit<User, "id">;
interface UserCreationError {
message: string;
}
function createUser(
creationRequest: UserCreationRequest,
): Result<User, UserCreationError> {
// snip...
}
createUser({ username: "bob.builder" })
.bind((user: User) => {
console.log(`Successfully created new user "${user.username}"!`)
})
.bindErr((err: UserCreationError) => {
console.error(`Unable to create user: ${err.message}`);
})
;
At every point the result of createUser
has a defined value and you
can’t just try and access the User
that was created until you decide
on how you’re going to handle the error.
And here’s the possible implementation of such a type.
type ResultContents<T, E> = { value: T } | { err: E };
class Result<T, E> {
public static ofValue<T, E>(value: T): Result<T, E> {
return new Result<T, E>({ value });
}
public static ofError<T, E>(err: E): Result<T, E> {
return new Result<T, E>({ err });
}
private readonly contents: ResultContents<T, E>;
private constructor(contents: ResultContents<T, E>) {
this.contents = contents;
}
public map<ReturnType>(transform: (value: T) => ReturnType): Result<ReturnType, E> {
if ("err" in this.contents) {
return Result.ofError<ReturnType, E>(this.contents.err);
}
return Result.ofValue(transform(this.contents.value));
}
public mapErr<ReturnErrType>(transform: (err: E) => ReturnErrType): Result<T, ReturnErrType> {
if ("value" in this.contents) {
return Result.ofValue<T, ReturnErrType>(this.contents.value);
}
return Result.ofError(transform(this.contents.err));
}
public bind(operation: (value: T) => void): Result<T, E> {
if ("err" in this.contents) {
return;
}
operation(this.contents.value);
return this;
}
public bindErr(operation: (err: E) => void): Result<T, E> {
if ("value" in this.contents) {
return;
}
operation(this.contents.err);
return this;
}
public unwrap(): T {
if ("err" in this.contents) {
throw new Error("cannot call unwrap on error result");
}
return this.contents.value;
}
public unwrapErr(): E {
if ("value" in this.contents) {
throw new Error("cannot call unwrapErr on success result");
}
return this.contents.err;
}
}
Just slightly more complicated than the Maybe
type but very much the
same in terms of idea.
📝 I could have implemented this with the
Maybe
type being in charge of the error and value conditions but I think in this case that would have cluttered things up since the implementation is already pretty small and I was able to leverage typing better with this solution.
Wrap-up⌗
There are tons of other monads out there but honestly most of them make things too complicated in my mind and I stick with these. They solve the majority of problems I encounter, are relatively easy to understand, take little to work into existing code, and don’t require a PHD or mastery of Haskell to explain to someone.