Pueden leer la versión en español aquí.
Oh the infamous M word. The one we don't speak about in javascript. Well, today we are going talk about it, specifically we are going to "review" one definition I really like, the only one that doesn't make my head explode. In order to keep our sanity we are just going to explore the aspects we can model using javascript. Everyone ready? Let us begin.
Here it is. This is the easy one, I swear. Monads are...
pointed functors that can flatten.
You said you were ready. Anyway, we can do this. Once you understand the behaviour of a functor the rest will fall into place.
Enter Functors
From a javascripty point of view you can think of them as containers with a very special feature: they allow you to transform their inner value in any way you see fit without leaving said container.
Isn't that intriguing? How would that look like in code. Let's try to make the simplest functor we can think of.
The Box
function Box(data) {
return {
map(fn) {
return Box(fn(data));
}
}
}
What happens in here? Well, we created a Box
specifically designed to hold a data
value and the only way to gain access to the value is through the map
method. This map
thing takes a function fn
as an argument, applies that function to data
and puts the result back in another Box
. I must tell you that not all functors look like this, but in general this is the pattern they all follow. Let's use it.
const xbox = Box('x');
const to_uppercase = (str) => str.toUpperCase();
xbox.map(to_uppercase).map(console.log);
// => X
// => Object { map: map() }
So, that Box
seems um... useless. Yeah, that's by design but not mine, this is actually the Identity
functor. It may not be useful in our day to day coding but for educational purposes it works like a charm.
What is the benefit of these functor things? By adding this tiny layer of abstraction we can separate an "effect" from a pure computation. To illustrate this let's take a look at one functor with an actual purpose.
A familiar face
You may or may not know this already but arrays follow the pattern I have described for the Box
. Check this out.
const xbox = ['x'];
const to_uppercase = (str) => str.toUpperCase();
xbox.map(to_uppercase);
// => Array [ "X" ]
The array is a container, it has a map
method which allows us to transform the value it holds inside, and the transformed value gets wrapped again in a new array.
Okay, that's fine, but what is the "effect" of an array? They give you the ability to hold multiple values inside one structure, that's what they do. Array.map
in particular makes sure your callback function is applied to every value inside the array. It doesn't matter if you have a 100 items in your array or none at all, .map
takes care of the logic that deals with when it should apply the callback function so you can focus on what to do with the value.
And of course you can use functors for so much more, like error handling or null checks, even async tasks can be modelled with functors. Now, I would love to keep talking about this but we have to go back to the monad definition.
The Pointed part
So, we need our functors to be "pointed". This is a fancy way of telling us that we need a helper function that can put any value into the simplest unit of our functor. This function is known as "pure", other names include "unit" and "of".
Let's look at arrays one more time. If we put a value into the simplest unit of an array, what do we get? Yes, an array with just one item. Interestingly enough there is a built-in function for that.
Array.of('No way');
// => Array [ "No way" ]
Array.of(42);
// => Array [ 42 ]
Array.of(null);
// => Array [ null ]
This helper function is specially useful if the normal way of creating your functor is somewhat convoluted. With this function you could just wrap any value you want and start .map
ping right away. Well... there is more to it, but that's the main idea. Let's keep going.
Into the Flatland
Now we are getting into the heart of the problem. Wait... what is exactly the problem?
Imagine this situation, we have a number in a Box
and we want to use map
to apply a function called action
. Something like this.
const number = Box(41);
const action = (number) => Box(number + 1);
const result = number.map(action);
Everything seems fine until you realise action
returns another Box
. So result
is in fact a Box
inside another Box
: Box(Box(42))
. And now in order to get to the new value you have to do this.
result.map((box) => box.map((value) => {/* Do stuff */}));
That's bad. No one wants to work with data like that. This is where monads can help us. They are functors that have the "ability" to merge these unnecessary nested layers. In our case it can transform Box(Box(42))
into Box(42)
. How? With the help of a method called join
.
This is how it looks like for our Box
.
function Box(data) {
return {
map(fn) {
return Box(fn(data));
},
+ join() {
+ return data;
+ }
}
}
I know what you're thinking, it doesn't look like I'm joining anything. You may even suggest that I change the name to "extract". Just hold it right there. Let's go back to our action
example, we are going to fix it.
const result = number.map(action).join();
Ta-da! Now we get a Box(42)
, we can get to the value we want with just one map
. Oh come on, you're still giving me the look? Okay, let's say I change the name to extract
, now it's like this.
const result = number.map(action).extract();
Here is the problem, if I read that line alone I would expect result
to be a "normal" value, something I can use freely. I'm going to be a little bit upset when I find I have to deal with a Box
instead. On the other hand, if I read join
, I know that result
it's still a monad and I can prepare for that.
You may think "Okay I got it, but you know what? I write javascript I'm just going to ignore these functor things and I won't need monads". Totally valid, you could do that. The bad news is arrays are functors, so you can't escape them. The good news is arrays are monads, so when you get into this situation of nested structures (and you will) you can fix that easily.
So, arrays don't have a join
method... I mean they do, but it's called flat
. Behold.
[[41], [42]].flat();
// => Array [ 41, 42 ]
There you go, after calling flat
you can move on without worrying about any extra layer getting in your way. That's it, in practice that's the essence of monads and the problem they solve.
Before I go I need to cover one more thing.
Monads In Chains
It turns out this combination of map/join
is so common that there is actually a method that combines the features of those two. This one also has multiple names in the wild: "chain", "flatMap", "bind", ">>=" (in haskell). Arrays in particular call it flatMap
.
const split = str => str.split('/');
['some/stuff', 'another/thing'].flatMap(split);
// => Array(4) [ "some", "stuff", "another", "thing" ]
How cool is that? Instead having an array with two nested arrays, we have just one big array. This is so much easier to handle than a nested structure.
But not only does it save you a few keystroke but it also encourage function composition in the same way map
does. You could do something like this.
monad.flatMap(action)
.map(another)
.map(cool)
.flatMap(getItNow);
I'm not saying you should do this with arrays. I'm saying that if you do make your own monad, you can compose functions in this style. Just remember, if the function returns a monad you need flatMap
, if not use map
.
Conclusion
We learned that monads are just functors with extra features. In other words they are magical containers that... don't like to hold other containers inside? Let's try again: they are magical onions with... nevermind, they are magical, let's leave it at that.
They can be used to add an "effect" to any regular value. So we can use them for things like error handling, asynchronous operations, dealing with side effects, and a whole bunch of other things.
We also learned that you either love them or hate them and there is nothing in between.
Sources
- Professor Frisby's Mostly Adequate Guide to Functional Programming. Chapter 9: Monadic Onions
- Funcadelic.js
- Fantasy Land
Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.
Top comments (3)
It is hard to understand monads if you don't talk about applicatives. Monads are neither there to handle computaional effects nor side effects. Types like
Maybe
/IO
do this. Functors are useful, because you can reuse your pure functions for all these types that handle computations with effects. Applicatives/Monads are useful, because you can compose several effectful computations. While applicatives don't assume an order these computations are evaluated in, monads do assume one.I did write something after I mentioned pointed functors, but then I deleted that section.
You're right, that's not the purpose of a monad, but they can be used to handle that sort of things. I mean, the types you mention
Maybe
andIO
are an example of that. I guess that's just me trying to tell other people that monads aren't useless in javascript.Most people need several years to really understand all facettes of Monads 'n stuff. During this journey they evolve different abstract representations in their head, where each becomes more accurate than the previous one. So there is really nothing wrong with your attempt to teach people the concept. I just wanted to point out some inaccuracies, that's all.