The term monad could sound weird to a software engineer because it comes from category theory. There’s plenty of related maths material around but let’s just forget about it and about the choice of that word (BTW am I the only one being triggered Leibniz memories from high-school philosophy classes?): I’m interested in a software engineering perspective on the topic and, since I couldn’t find an introductory one that was clear enough for me, I decided to take a dive in and build my own.
What is a monad?
Very, very shortly: a data structure implements a monad interface iff it defines some “lifted-up” functions sequencing operator (think of a functional “
So a monad itself is basically an interface meant to chain calculations that maintains some kind of additional “out-of-band” stuff, while still carrying this “context” along the way (here’s why I talk about “lifted-up” sequencing). For example they can carry state, IO conditions, error conditions, data structure markers, tags… Whatever.
Every interface having (at least) two specific methods (I’ll call them
passMonadThrough) and satisfying certain “carry on” rules (we’ll see them in a moment) can be called a “monad”.
What are they useful for?
Monads are useful because in some circumstances they simplify and clarify code. They let you focus on the main functional transformation flow without being distracted by “contextual” information. Think of a “nil-checking” monadic sequencing operator that will nil-check results for you at every function application step and will short-circuit the transformational pipeline if at some point a
nilvalue is produced.
An implementation of monads for Clojure is available as algo.monads, while a tutorial and some neat examples in Clojure have been written by Konrad Hinsen.
Under a monad’s cover
Let’s have a look at the interface definition and then at the “monadic behaviour” rules: I’ll use a lisp-style notation, except for the infix
x -> bodystyle for anonymous functions with a single
C1 === C2means that the computation
C1behaves the same as computation
(buildMonad v): adds some context to a value
v, building a new monad
(m passMonadThrough f): it takes a value already enriched with a context (i.e. the monad
m), a valid transformation of a “naked” value into something else with the same kind of context (i.e. the function
f) and hooks the two in some implementation-specific way, but still observing the following “carry on” rules:
((buildMonad v) passMonadThrough f) === (f v): passing a value just enriched with a context (so, a newly built monad) through a context-injecting transformation will behave like applying the function to the simple-value part. This is the first monad constructor “neutrality” rule, called “left identity” (
buildMonadis on the left of
(m passMonadThrough buildMonad) === m: a context-enriched value (i.e., the monad
m) passed through
buildMonadshould stay unchanged both in value and context. This is the second monad constructor “neutrality” rule, called “right identity” (
buildMonadis on the right of
((m passMonadThrough f) passMonadThrough g) === (m passMonadThrough (x -> ((f x) passMonadThrough g)): a context-enriched (i.e. monad
m) value passed through some context-injecting transformation (i.e. function
f), and then passed through some other context-injecting transformation (i.e. function
g) will yield the same result as passing the initial context-enriched value
passMonadThrough-mediated composition of
g(and NOW I realize it’s much more easily understood by reading it than by explaining it…). This is the rule implementing “chaining” and it is called “associativity” (we’ll see in a moment why).
Basically the “chaining” rule mandates that the
passMonadThroughmethod smash an input monad into any monad-building function (i.e. context-injecting transformation) in such a way that the result is “flattened” and doesn’t change the structure of the input monad’s “context”. This ensures that it can “pile up” any number of monad-building calls in a “last applied, last written” order.
passMonadThroughmust do so in an associative fashion, so that there’s no need to specify precedence about subsequent calls: this means they can just be written in a straightforward sequence.
The identity-related laws, finally, ensure that the monad constructor
buildMonadis neutral w.r.t. to
passMonadThroughboth when it’s on the left side and when it’s on the right side, which completes the “smooth” sequential behaviour.
If the above starts feeling like re-building a sequential, imperative-style control flow from functional programming and these “monad” interfaces, you are on the right track.
Example: lists as monads
Here’s a nice example: the list data structure can be turned into a monad by providing adequate monadic operations on it.
In the following,
lis a list,
vs are values and both
gbuild a list of some kind from a value. I’ll shorten
(build v) = [v] (l through f) = (concat (map f l))
Let’s now verify the 3 rules (being
l = [v]and
(f v) = l').
1) Left identity
[v] through f = (concat (map f [v])) = (concat [l']) = l' = (f v)
2) Right identity
[v] through  = (concat (map  [v])) = (concat [v]) = [v]
(l through f) through g = (concat (map f l)) through g = (concat (map g (concat (map f l)))) = (concat (map g (concat [[v1'] ... [vn']]))) = (concat (map g [v1' ... vn'])) = (concat [[v1''] ... [vn'']]) = [v1'' ... vn'']
l through (y -> ((f y) through g)) = (concat (map (y -> ((f y) through g)) l)) = (concat (map (y -> (concat (map g (f y)))) l)) = (concat [((y -> (concat (map g (f y)))) v1) ... ((y -> (concat (map g (f y)))) vn)]) = (concat [(concat (map g (f v1))) ... (concat (map g (f vn)))]) = (concat [(concat (map g [v1'])) ... (concat (map g [vn']))]) = (concat [(concat [v1'']) ... (concat [vn''])] = (concat [[v1''] ... [vn'']]) = [v1'' ... vn'']
Few more notes
Some languages have specific syntax to use monadic interfaces (e.g. Haskell) while others can build monadic DSLs quite easily (e.g. Clojure). Some languages with monad implementations have rigorous compile-time type-systems (e.g. again Haskell) and others are dynamic (Clojure).
Please note that the monadic behaviour rules are specified in terms of runtime behaviour, so typically the developer has to ensure that they hold with little or no compiler support.
I hope the above will help people (like me) looking for a less mathematical and a more computational perspective on monads!