Monads are a useful tool in functional programming. Unfortunately, they're a pain in the ass to understand. I don't think they're actually that complicated, it's just that nobody seems to explain them well.
A monad is a collection which implements a flatMap
method. That's it.
flatMap
is a function made out of flatten
and map
:
Collection.prototype.flatMap = function(f) { return this.map(f).flatten(); }
Which is to say, a monad is a collection with a map
method and a flatten
method.
Now, if you read the haskell wiki you'll find a lot of stuff about bind
and return
.
bind
is another name for flatMap
. return
(or pure
) is another name for the constructor.
There's a couple of extra rules, known as the monad laws. These basically state that flatMap
behaves
in the way you'd expect: if you pass flatMap
the constructor it should return the same value as
you started with because you're just wrapping and then unwrapping the value. Things like that.
You should already be familiar with many data structures which implement map
and flatten
,
such as lists and arrays. A less well-known data structure which implements the monad interface is known
variously as Option
or Maybe
. This data structure is a zero-or-one element list: either it
contains a value or it doesn't. These are usually represented as Some(value)
or None
when
the data type is called Option
, and Just(value)
and Nothing
when it's called Maybe
. It's the same thing.
The map
method looks like this: Some(value).map(f)
returns Some(f(value))
whereas
None.map(f)
returns None
again. Meanwhile, flatten
turns Some(Some(value))
into
Some(value)
while turning None
and Some(None)
into None
. So, roughly speaking, flatMap
means
"If this contains a value, use this function which will return another Option
. Unwrap it sensibly."
You would use flatMap
in this way to handle potential failure. For example, suppose you're doing some mathematical
operations and might divide by zero.
// Javascript
// Ignore the setup if you don't understand it.
// Define flatMap in terms of map and flatten
var Monad = {
flatMap: function(f) {
return this.map(f).flatten();
}
}
// None doesn't need values so it's a bare object
var None = {
map: function() { return None; },
flatten: function() { return None; },
name: "None"
}
Object.setPrototypeOf(None, Monad);
// Some needs values, so it's a constructor
function Some(x) {
this.value = x;
}
Some.prototype = {
map: function(f) {
return (new Some(f(this.value)));
},
flatten: function() {
if (this.value.constructor == this.constructor) {
return this.value;
} else {
return None
}
}
}
Object.setPrototypeOf(Some.prototype, Monad);
// The example proper
function reciprocal(x) {
if (x == 0) {
return None;
} else {
return new Some(1 / x);
}
}
var x = new Some(10);
var y = new Some(0);
// Let's try it out
x.flatMap(reciprocal); // Some {value: 0.1}
y.flatMap(reciprocal); // None
x.flatMap(reciprocal).flatMap(reciprocal); // Some {value: 10}
y.flatMap(reciprocal).flatMap(reciprocal); // None
reciprocal(10).map(function (x) { return x * 2 }); // Some {value: 0.2}
function double(x) { return x * 2; }
function half(x) { return reciprocal(x).map(double).flatMap(reciprocal) }
half(9) // Some {value: 4.5}
In this way, we can gracefully handle the possibility of division by zero. It's another way of returning
undefined
or null
except we never need to check if there's a value to use because flatMap
handles
that magic for us.
While collections implement the monad interface, they're not the only things that can. Functions, for example,
can do the same thing - so long as they're functions with a legible definition of flatten
. You could view
an array as a function: when you say arr[9]
is it so different from arr(9)
?
The most common example
you've likely encountered is Promises. Promises allow you to handle asyncronous execution by specifying a
function to run when the data gets back: fetchData().then(extractBody).then(function (x) { return int(x) * 9; })
and so on. .then()
is a hybrid of map
and flatMap
: if you pass it a function that returns a Promise, it
extracts the value for you.
The real deep magic of monads is where they start to be used for function composition. But the basic idea,
which is never expressed clearly and makes it seem drastically complicated from the very start, is really
quite simple: monad is an interface. "A monad" is a thing which implements that interface. That interface
is that you have map
and flatten
methods that behave correctly in relation to each other. That's all.