Monads
Introduction
For the past few months, I've been trying to write a blog post about monads, but every time I start writing I get stuck. At this point I'm pretty frustrated, so I'm scrapping my original idea. Instead of giving a coherent explanation of monads, which I'm not qualified to do, I'm going to try* to describe the intuition behind them. Hopefully after reading this, you'll be able to:
- Sound semi-knowledgeable when talking to other people who are interested in functional programming languages
- Be primed for other, more in-depth monads explanations (linked below)
Here Goes Nothing
Monads are parametrized data types. A parametrized data type is a data type that accepts another data type as an argument. For example, vectors in C++ are parametrized data types:
std::vector<int> my_vector;
In this case, we are passing the int
data type as an argument to the vector
data type, which essentially means that we are creating a vector of integers.
There are many different monads, so we'll use the option
monad in our example. Since monads are parametrized data types, an option is actually an option
of t
, where t
is any type, such as an integer. Therefore, you can think of a option<int>
as a integer, wrapped in the context of an option.
Monads must provide a way to interact with the underlying type. In other words, given a function f
that takes integers as input, there must be some way to apply that function to a option<int>
as well. The extra context provided by the monad, however, can affect the output of the function. This is what makes monads so powerful.
Specifically in the case of a option
monad, the context specifies whether the data actually exists or not. For example, a option<int>
could be Some(1)
, or it could be None
. You can think of Some
and None
as two sub-classes of option
s. Now, given a function f
acting on an object x
with type option<int>
, the output should be f(a)
if x = Some(a)
and None
if x = None
.
This is all fine and dandy but the explanation so far probably doesn't make any sense so I'm going to try to write some pseudocode to give you the idea. Let's say we had some functions with integer inputs that either returned a new value if the input passes a check or None
if it fails:
def f(x):
if x < 5:
return None
else:
return 2*x + 1
def g(x):
if x % 2 == 1:
return None
else:
return x / 2
def h(x):
if x == 3:
return None
else:
return x * 3
Then, if we wanted to compose the two functions, we'd have to write the following ugly code:
def hgf(x):
t = f(x)
if t is None:
return None
else:
t = g(t)
if t is None:
return None
else:
return h(t)
This code is ugly because we have to check at each stage whether the previously computed result was invalid, and if so terminate the computation. How could we rewrite this with monads? First, let's set up the code for the option monad. Note that the following is python pseudocode:
class Option:
"""Abstract supertype representing option<int>"""
def __>>=__(self, f):
"""
The infix left-associative 'bind' operator.
f: int -> Option
x >>= f :=
match x with
| Some(a) -> f(a)
| None() -> None()
"""
if type(self) == Some:
return f(self.x)
else:
return None()
class Some(Option):
def __init__(self, x):
self.x = x
class None(Option):
"""Note that this is different from the None keyword in python"""
def __init__(self):
pass
In this code, we introduce the made up python infix operator >>=
, which is technically called bind. You can think of x >>= f
as equivalent to what we described earlier: f(a) if x == Some(a) else None
. We also have to slightly rewrite our functions to satisfy the type signature int -> Option
:
def f(x):
if x < 5:
return None()
else:
return Some(2*x + 1)
def g(x):
if x % 2 == 1:
return None()
else:
return Some(x / 2)
def h(x):
if x == 3:
return None()
else:
return Some(x * 3)
Now that we've set everything up, how do we compose the functions together? Well, it becomes amazingly simple:
def hgf(x):
return Some(x) >>= f >>= g >>= h
It's practically a one-liner!
Intuition
To summarize, monads are software patterns that allow programmers to write clean code to handle chained computations by specifying default behavior in the "error" case, where the specific behavior and error case depends on the monad in use. This way, the programmer does not have to check for the error case explicitly at each step in the computation. In addition to Option
, another common monad is Deferred
, which represents the result of an asynchronous computation (the error case is when we have not finished computing the result). As you can imagine, the Deferred
monad turns callback hell into callback heaven:
// this is sad
AsyncGetX(function (x) {
AsyncGetY(x, function (y) {
AsyncGetZ(y, function (z) {
console.log(z);
}
}
}
// this is nice
x >>= AsyncGetX >>= AsyncGetY >>= AsyncGetZ >>= console.log
Disclaimer
I have not provided a formal definition of a monad, nor enumerated all of their use cases. These are simply my observations of where monads come most in handy.
Further Reading
- Escaping Hell with Monads. My personal favorite, goes over a couple applications of monads without assuming too much functional programming background.
- Translation from Haskell to JavaScript of selected portions of the best introduction to monads I've ever read
- You Could Have Invented Monads!. The "best introduction to monads" mentioned above. Requires some more knowledge of type systems.