Remember when we talked about
map? We only used map on arrays, but you can actually implement
map on other data structures, e.g. trees, streams, promises...
Any type that has a map function is a functor.
// identity myFunctor.map( x => x) === myFunctor // composition === chaining myFunctor.map( x => f(g(x)) ) === myFunctor.map(g).map(f)
In the code above, I used
===to keep it simple, but I should have checked for deep equality. If you don't know what this means, don't worry, you should be able to understand the meaning anyway. Nonetheless, if you want to learn more about it, this StackOverflow thread is a good place to start.
The advantage of using a functor is that the container is now abstracted away. E.g., in streams, we don't need to care about asynchronous data handling, we can just use a stream like an array.
Another cool feature of functors is that you can chain
map calls, because the
map function returns another functor.
So why don't we create our own functor?
Giving a definition of monad is somewhat tedious and requires a bit of theory, so we are first going to build an intuition for them through examples.
Let's write a function that duplicates every item in an array (e.g.
Let's first try with map:
Running the code, you can see that the result is not quite what we wanted:
[[1,1],[2,2],[3,3]]. Wouldn't it be great if we had a function that automatically "flattened" the array we returned into
As you can see, flatMap's callback returns another monad and flatMap's job is handling the unpacking. If you are interested in the details of the implementation, here's my code:
Although the array's implementation of
flatMap is very interesting and useful, it's not the only one: we can, for example, use flatMap to maintain knowledge of previous states of a system. As always, let's start with an example.
Let's say we want to create a model for a gambler: the gambler plays multiple times at the roulette; sometimes he wins, sometimes he loses. But if he goes bankrupt, he gets kicked out of the casino and cannot recover. We want to simulate a number of games and see if at the end the gambler still has money or not.
We cannot simply add up wins and losses, because we would ignore intermediate states. The following is a wrong solution:
What we need is a model that stops computing the transactions after he went bankrupt, like the following:
This time, we used
flatMap to remember past states, but again the callback returned a new monad and
flatMap handled the unwrapping of the original monad content.
Monads are a broad definition, but they all share one characteristic: they implement
flatMap, which in term is useful to maintain knowledge of the context in which a function is being executed.