What are monads? How do you create them in JavaScript, and how do you use them? What problems do they solve? Why should one care about monads and add them as a tool for development? This article will start with basic requirements to show what monads are and how to create them, and we'll later see how and when to use them.
Session Replay for Developers
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — an open-source session replay suite for developers. It can be self-hosted in minutes, giving you complete control over your customer data.
Happy debugging! Try using OpenReplay today.
Using the .map()
method allows for shorter, clearer, declarative-style programming, so we would want to be able to use it more. In a previous article, we saw that we could extend individual types to allow adding this method to them. However, that is sort of limited and requires repeating the definition for each type. So, we'll work in a different, more general way and eventually get to monads, which are way more powerful than just "types with maps"!
- We'll start by defining containers, which work with any type
- We'll then add some features and get more power with functors
- We'll finish by adding some more capabilities and producing monads
We could do this using classes, but we'll go with vanilla JavaScript, which is as basic and compatible as possible. We'll be using object constructors instead of classes, and closures instead of attributes, as you'll see.
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. Douglas Crockford
First Step: Defining Containers for all data types
We want something that will contain a value of any type and (at the very least) allow us to apply a .map()
method to it. The following code does just that:
function Container(x) {
this.map = (fn) => fn(x); // ➊
this.toString = // ➋
() => `${this.constructor.name}(${x})`;
this.valueOf = () => x; // ➌
}
Let's study the code. The method we care about is .map()
; given a fn
function, it applies it to the container's value x
➊.
Note: The
x
value is not stored as an attribute but rather accessed through a closure, making the value inaccessible from the outside.
We added a couple more methods. The standard .toString()
method is overridden to get a more accurate description of the container and its value ➋. Finally ➌, the .valueOf()
method is just a help for debugging, because you could easily write someContainer.map(console.log)
and see the container's value.
How do we use these containers? We can create a container with a value of any type; an example follows.
const plus1 = (x) => x+1; // ➊
const ccc = new Container(22); // ➋
console.log(plus1); // ➌ 23
ccc.valueOf(); // ➍ 22
ccc.toString(); // ➎ Container(22)
First ➊ we define an auxiliary plus1()
function that we'll use in our tests. Then ➋ we create a container for a numeric value, 22. The following line (written in pointfree style) shows how to use .map()
➌ with the same notation used for arrays. Finally, the last lines ➍ ➎ illustrate our additional methods; we see that the original ccc
container wasn't modified and still has the 22 value inside.
OK, these containers work, but they are roughly equivalent to variables. We wanted to be able to use .map()
, and we managed that, but we cannot chain operations as we did in our previous article on fluent interfaces, so we must work a bit more, and go from containers to functors.
Second Step: Adding more power by using Functors
Containers are good, but we need .map()
to return a container so we can chain more operations. By changing the definition of the operation, we have moved to a new kind of object, a functor.
Essentially, a functor is just a container (as we've created it), but we need its .map()
operation to return a new functor, not just a value. This is precisely what happens with arrays: when you use .map()
, you produce a new array, so now we realize that arrays are functors! The needed change is small.
function Functor(x) {
Container.call(this, x); // ➊
this.map = (fn) =>
new this.constructor(fn(x)); // ➋
}
We start ➊ by invoking the Container
constructor, so our functors will share their three methods. The following line ➋ overrides the inherited .map()
method with a new implementation that returns a new functor, allowing us to chain operations as in the example below.
const plus1 = (x) => x+1;
const ccc = new Functor(22); // ➊
const ddd = ccc.map(plus1).map(plus1); // ➋
ddd.map(plus1).valueOf(); // ➌ 25
ddd.toString(); // ➍ Functor(24);
This example is like the one for containers, but instead of creating a container ➊ we built a functor. This allows us ➋ ➌ to chain several operations. The last line ➍ shows that ddd
is a functor containing 24.
These functors are better suited for coding than our plain containers, but there still are a couple of kinks we must work out; let's go on to monads!
Third (and last) Step: Get maximum flexibility by using Monads
There's a problem we still need to consider... Let's assume a fun
functor and a makeFun(…)
function that returns a functor. What would fun.map(makeFun)
produce? The answer: we'd get a functor whose value would be a second functor! This would make coding very difficult; to apply a function to the value in the second functor you would have to write code such as fun.map(makeFun).map((x) => x.map((y) => someOtherFunction(y)))
; neither practical nor clear!
We will extend functors to create a new type, monads. We want monads to be able to "unwrap" the value a function returns (if needed) so we won't get values wrapped two or more times. We will have a new unwrap()
(internal) function, and a .chain()
method that will use it to unwrap possibly wrapped values.
function Monad(x) {
Functor.call(this, x); // ➊
const unwrap = (z) => z.chain ?
unwrap(z.valueOf()) : z; // ➋
this.chain = (fn) =>
new this.constructor(unwrap(fn(x))); // ➌
}
Monads are functors, so we start ➊ by calling the corresponding constructor. Our unwrap()
function ➋ detects if its argument is a monad by seeing if it has a .chain()
method; other ways of doing this detection could be used. The .chain()
method ➌ is precisely like .map()
, but it takes care of unwrapping whatever is returned by fn(x)
, so the resulting monad won't be wrapping anything more than once.
Working with monads is, so far, exactly the same as with functors. The only difference is that you would probably prefer using .chain()
to avoid double wrapping for values, but in most cases, .map()
will do.
Functions in monads
So far, we have been storing "common" values in monads, like numbers. But, as with variables, we could think of storing a function in a monad, like in the example below. Would that work? Of course, it should, but how would we use the contained function?
const nnn = new Monad(22); // ➊
const add = (x) => (y) => x+y; // ➋
const fun = nnn.map(add); // ➌
We first create a monad ➊ containing a number. The add()
function, written in curried style as usual in functional programming. When we apply add()
to the monad ➌ we get a new monad whose value is a function that will add 22 to whatever it's applied to. But how will we be able to do that? We could write something awkward like the following, using two .map()
operations, but that's not clear or practical!
const ooo = new Monad(9);
const ppp = ooo.map((x) => fun.map((f) => f(x)));
Instead of that roundabout way of working, we'll define a new .ap()
method that will allow us to apply a function in a monad to any value that allows mapping.
function Monad(x) {
// ...all the previous code, plus:
this.ap = (v) => v.map(x); // ➊
}
fun.ap(ooo); // ➋ Monad(31)
We need just one line ➊ to define the new .ap()
method, which will apply the contained function to a given value v
by mapping. We can see this working ➋ more clearly; we can now apply the function in the fun
monad to another monad (ooo
) without complications.
Note: you could want to write something like
fun.ap(9)
, but that wouldn't work because plain numbers don't provide a.map()
method. Can you fix this so.ap()
will also work with numbers, strings, booleans, etc.? We'll leave this up to you as an exercise!
Conclusion
We have now reached our first goal; we implemented monads! We started with basic containers, made a small change to get functors, and added more power to get monads. We did all of this using vanilla JS, starting from basic requirements.
However, just showing how monads are defined is interesting, but the real deal is seeing why and how they simplify coding, dealing with errors, working with missing values, and more -- we'll get to that in part 2; stay tuned!
Top comments (0)