paul shen
Posts

Monads for JavaScript developers

Why are there so many monad articles and tutorials? Because people like me keep writing them 😎

Maybe it's because of the monad tutorial fallacy. Anyways, I hope this gives you, a JavaScript developer, a gist of what monads are and why people care about them.

This article is also a small experiment. Try clicking this dashed link. It will open a pane, as will the other dashed links. If you are at a computer (recommended), you'll find some interactive exercises as well.

I imagine you are familiar with the almighty JavaScript and have encountered types before, maybe with TypeScript or Flow. If not, the code examples should still be intelligible. No Haskell experience required!

Building intuition with JavaScript

Let's pretend for a moment that JavaScript is a pure language and side-effects are not allowed in functions.

function increment(x: number): number {
console.log('incrementing'); // side effect not allowed!
return x + 1;
}

How can we implement something similar to console.log without side effects? We could wrap the return value to include a string.

function incrementWithLog(x: number): [number, string] {
return [x + 1, 'incrementing\n'];
}
function run() {
const valueWithLog = incrementWithLog(0);
const [value, log] = valueWithLog;
// value = 1
// log = 'incrementing\n'
}

Notice how we've created a context around the original value. Where we started with increment returning a number, incrementWithLog now returns a "tuple" [number, string] where the string represents the log message. Let's call this context WithLog<T> = [T, string] where T could be any type. In this example, incrementWithLog returns a WithLog<number>, with T being the number.

What if we want to use this function multiple times?

function run() {
const initialValue = 0;
// smooth sailing with the original increment without log
return increment(increment(increment(initialValue)));
}
function runWithLog() {
const initialValue = 0;
// each incrementWithLog includes a log
const [result1, log1] = incrementWithLog(initialValue);
const [result2, log2] = incrementWithLog(result1);
const [result3, log3] = incrementWithLog(result2);
return [result3, log1 + log2 + log3];
}

This is okay but there's extra work dealing with the log message. We need to destructure each return value. We can't chain calls nicely like we can with increment. Can we make WithLog easier to work with?

Let's introduce a couple new functions. The first is wrap, which takes a plain old value and puts it in our context with an empty log (represented by an empty string '').

type WithLog<T> = [T, string];
// Put a value into the context
function wrap<T>(value: T): WithLog<T> {
return [value, ''];
}

Our second function bind is more complicated. It takes two arguments, a WithLog<T> value and a function with type T => WithLog<T>.

// Apply a given function to a context value
function bind<T>(
valueWithLog: WithLog<T>,
f: T => WithLog<T>
): WithLog<T> {
const [value, existingLog] = valueWithLog;
const [newValue, newLog] = f(value);
return [newValue, existingLog + newLog];
}

It calls the given function with the value inside the existing context. It then concats the log strings together to form the new log. You can think of it as appending the new log message onto existing logs.

Exercise
Now it's your turn. Open this exercise and try implementing runWithLog using both wrap and bind.

View the solution. runWithLog2 looks a lot like our original run with just increment! There isn't any code dealing with the log messages. wrap and bind take care of that for us. We don't even have to know how WithLog is implemented. We know it's a tuple from above but that can change without affecting our implementation of runWithLog.

Quick recap

We just made our own WithLog monad! We haven't formalized any of this yet but I hope you have an idea of what monads feel like.

We have a context type WithLog<T> used to represent a value with a log message. We also defined two functions for working with WithLog. wrap puts a value in the WithLog context. bind applies a function T => WithLog<T> to a context value WithLog<T> to get another context value WithLog<T>.

Why learn monads?

For JavaScript developers, I don't think monads are that useful and are definitely not necessary to understand. However, ideas from functional programming are what inspired frameworks like React. Learning monads and alike gets you comfortable thinking about types at a higher level.

Haskell

TL;DR Any real Haskell program requires the use of monads.

Monads are usually associated with Haskell because they form the building blocks for writing programs. In the example above, we pretended that we couldn't have side effects inside functions. This is actually true in Haskell! You can't just add console.log inside your function.

The core Haskell programming language doesn't have many "features" that you take for granted in other languages. In JavaScript, you can put side effects anywhere. In JavaScript, global and module state is easy.

In most functional programming languages, you don't have imperative statements like you do in most popular programming languages. Instead, everything is an expression. In Haskell, the IO monad gives programmers the ability to sequence effectful actions. For example, putStr (the console.log equivalent) has type string => IO<void>.

Again, this is not a Haskell tutorial but the high-level picture is that combining monads allows Haskell programmers to add "features" to their programming environment. Our WithLog monad adds the feature of logging strings. We'll see how the Maybe monad below adds the feature of failure. People describe monads as "computational context". Haskell programmers get to (have to?) pick and choose what programming features to use.

Notes

In Haskell, our wrap function is called return. It's extremely confusing hence why I use wrap in this article. Just know that this is not the real name.

The function (second arg) given to bind can return a context value of another type parameter. bind has type (M<T>, T => M<U>) => M<U>. I restricted U = T in the bind example above to reduce the number of type variables. For example, we can do the following.

function isEvenWithLog(x: number): WithLog<boolean> {
return [x % 2 === 0, 'called isEven\n'];
}
function runWithLog(x: number): WithLog<boolean> {
return bind(bind(wrap(x), incrementWithLog), isEvenWithLog);
}

If you wanted to stop here, I don't blame you. Thanks for sticking this far! The rest of the article defines monads more precisely and gives you more context to understand monads.

What is a Monad exactly?

Now that we have interacted with monads, let's define them more precisely. Monad is a type class that is defined in Haskell as the following.

class Applicative m => Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b

Huh? This isn't a tutorial on Haskell; we want to learn as little Haskell as we need to understand monads. I'll break down the relevant pieces.

Monad is a type class with kind * -> *. Damnit paul

The approximate English translation: Monad is a generic type with one type parameter. It also has two associated functions, return and bind.

If you're familiar with a type system like TypeScript, monads roughly look like M<A> where M is the monad. For example, Array<T> has the structure to be a potential monad.

Let's translate the Haskell definition above into pseudo TypeScript. I've replaced return with wrap because return is a restricted JavaScript keyword (and extremely confusing). The >>= symbol is bind.

interface MonadImplementation<M<_>> {
wrap: <A>(A) => M<A>,
bind: <A, B>(M<A>, A => M<B>) => M<B>,
}

Let's revisit our WithLog context. Here, we plug in our implementation for wrap and bind.

type WithLog<T> = [T, string];
const WithLogMonadImplementation: MonadImplementation<WithLog> = {
wrap: <A>(x: A): WithLog<A> => [x, ''],
bind: <A, B>(m: WithLog<A>, f: A => WithLog<B>) => {
const [value, log] = m;
const [newValue, newLog] = f(value);
return [newValue, log + newLog];
},
};

This is the JavaScript implementation of our WithLog monad!

Maybe Monad

The best way to understand monads is to implement one (or a couple). Let's try implementing the Maybe monad.

type Maybe<T> = { value: T } | undefined;

This is a generic type representing potential failure. For example, Maybe<number> could either be a { value: number } or undefined (failure!). Why don't we just use T | undefined?

Exercise
Try implementing wrap and bind for Maybe.

wrap is straightforward; it puts the given value into the Maybe context. If the given value to bind is undefined (failure), we just continue failing by returning undefined. Otherwise, we call the given function, which will return us another Maybe value. Note that calling function f might "fail" and return an undefined.

If you have a sequence of computations and any of them fails, we want the whole sequence to fail. Here's an example.

function parseColorHex(color: string): Maybe<number> {
switch (color) {
case 'red':
return { value: 0xff0000 };
case 'green':
return { value: 0x00ff00 };
case 'blue':
return { value: 0x0000ff };
}
return undefined;
}
function getProfileColor(user: User): Maybe<string> {
const profile = profiles[user.profileId];
if (profile === undefined) return undefined;
return { value: profile.color };
}
function getUser(userId: string): Maybe<User> {
const user = users[userId];
if (user === undefined) return undefined;
return { value: user };
}
function main(userId: string): Maybe<number> {
return bind(bind(bind(wrap(userId), getUser), getProfileColor), parseColorHex);
}

We're sequencing three functions here: getUser, getProfileColor and parseColorHex. If anything fails (returns undefined), the entire sequence will fail (return undefined). Note that if we don't fail, we will end with a {value: answer} instead of {value: {value: {value: answer}}}.

Comparing to fmap

The idea of fmap is more common function in JavaScript. I'm going to skip some Haskell details but let's meditate on the following function.

// Apply f to every A inside m
fmap: (m: M<A>, f: A => B) => M<B>
// A common example
arrayMap: (arr: Array<A>, f: A => B) => Array<B>

fmap applies a function to values inside a context value. The most common example is mapping over an array.

Here's fmap for Maybe. I'm putting it side-by-side with bind.

function fmap<A>(m: Maybe<A>, f: A => B): Maybe<B> {
if (m === undefined) {
return undefined;
}
return {
value: f(m.value);
}
}
function bind<A>(m: Maybe<A>, f: A => Maybe<B>): Maybe<B> {
if (m === undefined) {
return undefined;
}
return f(m.value);
}

Spot the differences

They're so similar! The most notable difference is the type of f, the function parameter. The f for fmap returns B whereas f for bind returns Maybe<B>.

If you pass in a {value} to fmap, you're definitely getting a {value} back (never undefined). However, bind's given function can return undefined. bind's f has the ability to modify the "structure" of the context value. In a way, you can think of monad's bind as more powerful than fmap.

Nice to know

We’ll keep formalizing what monads are but if this is feeling tedious, remember that these concepts are not necessary in JavaScript!

Just satisfying the types is not enough to call it a monad. It also needs to satisfy a few rules so they act predictably. These are known as the Monad Laws.

Because working with monads is so common in Haskell, there is syntactic sugar called do notation.

Another way to think about monads is with join, a way to smash two "computational contexts" into one. For example, WithLog<WithLog<T>> => WithLog<T>.

Recap

A monad is a generic type that implements wrap and bind. Although the topic may be overhyped, monads are essential in some programming languages, most notably Haskell. Monads enable side effects and allow sequencing actions in a pure functional programming language.

I hope you found this useful. Please send any comments/feedback on twitter. Thanks!

If you want to learn more about monads and type classes, here are some links that I found useful.

September 03, 2020
Browse more posts or follow on Twitter.