DEV Community

Heiker
Heiker

Posted on • Edited on • Originally published at vonheikemen.github.io

An introduction to Monads (in js)

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));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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() }
Enter fullscreen mode Exit fullscreen mode

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" ]
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode

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 .mapping 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);
Enter fullscreen mode Exit fullscreen mode

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 */}));
Enter fullscreen mode Exit fullscreen mode

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;
+     }
    }
  }
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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 ]
Enter fullscreen mode Exit fullscreen mode

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" ]
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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


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.

buy me a coffee

Top comments (3)

Collapse
 
iquardt profile image
Iven Marquardt

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.

Collapse
 
vonheikemen profile image
Heiker

It is hard to understand monads if you don't talk about applicatives.

I did write something after I mentioned pointed functors, but then I deleted that section.

Monads are neither there to handle computaional effects nor side effects. Types like Maybe/IO do this.

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 and IO are an example of that. I guess that's just me trying to tell other people that monads aren't useless in javascript.

Collapse
 
iquardt profile image
Iven Marquardt

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.