Explained in 5 minutes: Monads in plain JavaScript
Despite the intimidating cover photo, I’ll try to explain monads using as little hard math as possible. No mentions of the category theory, no Kleisli, not even Curry. Although there will be a NaN
.
In the words of the great Douglas Crockford:
In addition to its being good and useful, it’s also cursed and the curse of the monad is that once you get the epiphany, once you understand — “oh that’s what it is” — you lose the ability to explain it to anybody else.
Well, I have a theory — this is because among all the excellent articles on this subject, which provide a great explanation of the mathematical theory behind monads, and how they should be implemented (along with exquisite examples!), one thing is amiss:
What do I actually need those for?
Before we try to find out, one remark. Some time ago, I read that you really shouldn’t try to understand monads based on anything that isn’t firmly based on explaining the math behind them and their place in the Category Theory. For those interested: there are some excellent articles covering that subject, like this one, or this one, or the one that introduced monads into programming from 1992. Since I really don’t want to repeat those here, please read (or at least scan) them now and come back here afterward (although I do believe that it’s not entirely necessary to understand what happens next).
OK, now we have that out of the way — what is a monad? Well, the simplest way to put it is — it’s a wrapper around some value. In terms of functional programming, a monad is a functor, a structure that allows applying a function to it. It’s an extra layer of abstraction between the value and whatever processing we want to put the value through. But that tells us nothing.
Let’s take a look at the simplest monad that you might imagine: an implementation of the Identity
monad:
const Identity = (value) => ({
join: () => value,
map: (fn) => Identity(fn(value)),
bind: (fn) => Identity(fn(value).value()),
});
As you can see, we implemented Identity
as a function that returns a simple object with three methods: join
, which only serves to return the value wrapped in the monad, map
, which is used to apply a function to the wrapped value.
Let’s look at the simplest possible example:
const addTax = (value) => value * 1.1;
const cartValue = Identity(100);
const cartValueWithTax = cartValue
.map(addTax) // At this point this is Identity(110)
.join(); // Equal to 110 as a number
OK, this is clear, but why should we even bother? Why can’t I just multiply the cartValue
as a number by 1.1
and get a number as a result like a normal human being? Well, this is the most crucial part, so:
The Identity
monad has — just as the identity
function in many functional programming libraries — a pretty transparent logic. It’s a placeholder monad, the same as identity
is a placeholder function. Applying addTax
to our value works well for the scenario given above, but let’s think about what would happen if the value of our cart would equal undefined
. Thanks to the wonderful way that JavaScript works, we will get a NaN
.
Usually, to get around this, the most intuitive way would be to add a check for the value to the addTax
function. This is a wrong approach. It obscures the purpose of the function and makes it less pure. The addTax
function should do just one thing and one thing only: add
the Tax
.
OK, wiseguy — you might say — how should we take the undefined
value into account? Well, that’s what we have monads for. We want to keep the function pure. We also want our logic to behave differently based on the value we pass to the function. The best place to define that behavior is the functor!
Let’s look at probably the most popular monad there is: Maybe
. This might be a basic implementation (I’m leaving the usual methods you might encounter in a monad like of
for clarity):
const Maybe = (value) => ({
join: () => value,
map: (fn) => value === undefined || value === null
? Maybe(null)
: Maybe(fn(value)),
bind: (fn) => value === undefined || value === null
? Maybe(null)
: Maybe(fn(value).value()),
});
Let’s see what happens when we try to apply addTax
to different values:
const primitiveCart = 100;
const undefinedCart = undefined;
const nullishCart = null;
const maybeCart = Maybe(100);
const maybeUndefinedCart = Maybe(undefined);
const maybeNullishCart = Maybe(null);
addTax(primitiveCart); // boring, 110
addTax(undefinedCart); // NaN
addTax(nullishCart); // 0 - thanks, JavaScript!
maybeCart.map(addTax).value(); // 110
maybeUndefinedCart.map(addTax).value(); // null
maybeNullishCart.map(addTax).value(); // null
Let me get this message as straightforward as possible again: this allows us to cater to the unpredicted behavior of the data without obscuring the function's logic. This is because we handle the behavior in a generic way (because we have generic monads for different situations) in a functor.
This, of course, is only a scratch at the tremendous wealth of possibilities that using monads brings. Let’s say that we want to greet every guest that enters our hotel:
const greet = (name) => `Hello, ${name}!`;
The problem is that we don’t know the names of every guest that enters our hotel. Typically, we’d add a conditional statement inside of our greet
implementation or wrap it in another function that handles that situation. But again — why do it in a function when we can in a functor?
Let’s take a look at just one more elementary example — the Optional
monad:
const Optional = (value) => ({
value: () => value,
orElse: (alternativeValue) => value
? Optional(value)
: Optional(alternativeValue),
map: (fn) => Optional(fn(value)),
});
Now let’s greet our guests without using monads:
const firstGuest = "John";
const anonymousGuest = undefined;
greet(firstGuest); // "Hello, John!"
greet(anonymousGuest); // "Hello, undefined" - this is a bit embarrasing
Now with the Optional
approach:
const OptionalWithStrangerFallback = (name) => Optional(name)
.orElse("stranger");
const firstGuestOptional = OptionalWithStrangerFallback(firstGuest);
const anonymousGuestOptional = OptionalWithStrangerFallback(anonymousGuest);
// I really hope you know where this is going at this point
firstGuestOptional.map(greet).value(); "Hello, John!"
anonymousGuestOptional.map(greet).value(); "Hello, stranger!"
The purpose of all of this is, again, to keep the logic pure. Over time (and remember — you in 3 months will be a very different person from who you are right now), it’s very, very easy to forget the fail-safes and other “workaround” code put into functions that should only contain business logic. This is way it’s much cleaner and maintainable.
I really hope this clears things up with monads. If not, please let me know in the comments! Also, if you’d like to read more articles on JavaScript, functional programming, computer vision, architecture, geometry, memes, technical leadership, or any mix of the above, be sure to follow me here or on LinkedIn!
I mentor software developers. Drop me a line on MentorCruise for long-term mentorship or on CodeMentor for individual sessions.