The Monad Interface
Monad Explained Simply
Monads are all-the-rage these days in the JS world. I’d be surprised if any serious JS developer hadn’t heard of Monads. But just in case you haven’t, I like to describe a monad as a fancy box. It’s a special box. You can’t (well, you shouldn’t be able to) see exactly what’s inside the box, but you can know that this box holds values of a certain type, and exposes three primary operators with which we can modify or take out the content of that box. Those operations are what I call the Monad Interface.
OK, so Monad isn’t exactly an interface. But in JS we can think of it as one. As a quick refresher, in a strongly typed language (such as the C family) an interface is the definition of what a class object has. If you’ve ever used TypeScript, you know that an interface describes the expected shape of something — like an object, or the parameters and return value of a function. What an interface boils down to is a contract to be upheld by anything that implements that interface, but it gives no specifics of the implementation. It’s useful to think of things as interfaces, because interfaces are abstract. Abstraction is one of the most useful concepts in computer science. To say something is abstract is to express how far away it is from machine code. The further away from machine code we can be the better because humans brains understand the abstraction of languages; only machines are made to read machine code. The best part about an interface is that it is a cost-free abstraction. If you write one in TypeScript, for example, it doesn’t even generate code in the end, it only compile-time checks that the interface’s contract is upheld. In bare JS, an interface is only in the mind of the author. So we can only uphold an interface’s contract by assuring on our own that all of the expected methods and fields are correctly implemented — there are no compile time checks to help you out. But that’s OK, because the dynamically-typed nature of JS comes with some other very nice benefits, which I won’t necessarily discuss just this moment.
So without further ado, here is the interface that Monad adheres to (in TS interface notation):
It’s actually not that complicated. I’ll go through each of these three functions and make them crystal clear.
First we’ll start with
map is the easiest function to grasp of the Monad interface. We use it all the time in JS. It states, ‘Given a monad
M a and a function
a -> b, apply that function to the contents of monad
M a producing a monad
M b. You see map in the wild in native JS’s Array type. In Array,
map simply applies the provided function to each element in the array, producing a new array:
Notice the way I call map with just the function
sq's identifier — that’s a declarative style of coding. You define things in one place and call them by name elsewhere. It actually makes things much less complicated to understand to call them simply by name, because it de-clutters the code you write. Plus, you might want to call
sq in more than one place. Why write out the anonymous function
x => x * x every time you want to call
sq? Also see how nice that is to read? It’s nearly plain English, ‘Apply function
sq to the contents of
a1 and assign that to a new array
unwrap is the most pointless-seeming of the Monad interface. You may know it as
emit, and probably others, but all it means is, ‘Take the contents out of the box’. Although it may seem like a silly thing to have, it is actually a really important function. If you couldn’t take the inner content out of the box, Monads would be pretty useless. That being said, you shouldn’t normally be calling
unwrap, and instead should opt for a function that considers every variant of your Monad.
Consider this snippet, defining a minimal Option (a monad that handles nullable values gracefully).
That snippet creates the basis defines a simple container with two variants (Some and None) called Option. It isn’t a full fledged Monad yet, because we haven’t defined
unwrap on it. Notice the definition of
or on the Option namespace-object. It calls
unwrap internally instead of directly. It checks for each variant, throwing an error if it isn’t a variant of Option. This is preferential to
unwrap because unwrapping a None value will simply return a None. Most Monads have a specific function for unwrapping them safely, considering null or undefined values and responding appropriately and, more importantly, predictably.
Let’s flesh out Option with
unwrap operators as we defined above.
Now we’ve got some real functionality. Let’s quickly discuss what this means:
- Option has two variants — Some and None.
- You can either have Some of something, or nothing at all.
- When you have Some of something, you can map and unwrap it just like normal.
- When you have None of something you can try to map or unwrap but you still just have None, so that’s why we keep returning it.
Now we can
map functions over the contents of our box. If we stopped at just
map, we’d have what is referred to as a Functor. A Functor is a fancy box that can
map. It’s easy to mistake a Functor for a full-fledged Monad, but they are different. There are mathematical Identity laws that must be upheld to properly define a a construct as a Monad, involving both the
chain methods. We won’t exactly get into all of that, but we will define
chain, which when defined properly along with
unwrap pretty much does make a Functor a Monad. Let’s define
See why I saved chain for last? It’s given to us for free by defining
unwrap on a type.
chain is really for chaining together functions that lift values into Monads, discarding the outer Monad by unwrapping. It’s a kind of mathematical symmetry that allows us to apply monad-constructing operations without getting a box-in-a-box-in-a-box kind of situation. Those are the types of complexities FP aims to avoid.
Now we can play with
chain a bit:
If we didn’t have
chain, we could still map functions like this. The problem with that approach is that then we end up with an
Option(Option(A)) instead of just an
Option(A). It’s much more complicated to work with boxes in boxes. Let’s just look at what would happen:
See how we now have to consider what our nested values are, and then add more and more unwrapping as we go? That’s why
chain is so very important — it makes an otherwise fairly verbose series of operations very simple and easy to understand. One can only process so much information at a time, so why not make life better for yourself by making the that information more expressive? It just makes sense. Imagine trying to process some retrieved JSON using a method like prop that lifts the result into a new monad — every nested value creates a new Monadic wrapper if we don’t use chain to unwrap each result. That would result in unwraps equal to the level of nesting you need to descend — not ideal. Moral of the story: use
chain to safely operate on your value while shedding the excess containers.
So now we know how to define simple Monads based on this interface. Here’s a pen that will let you hack at the Option Monad:
Hope you enjoyed an adventure in Functional Programming. Until next time, FP on folks!