Monads for Clojure programmers
This post is strictly about the programming technique. There is no philosophy or category theory. If you're interested in a more in-depth explanation of all the concepts related to monad, I have a longer series on the subject.
My goal with this post is not to convince you to start using monads everywhere; I only aim to make the technique easy to understand, using Clojure notation to explain it.
What is it for?
Monads are good for reifying side-effects while keeping your code pure. A
single monad can reify all possible side-effects (like the IO
monad in
Haskell), or you can make a small, bespoke monad to reify just the one
side-effect you want to allow in a specific context.
When spelled-out, monadic values (concrete instances of a specific monad) will look like an imperative DSL, with each line binding the return value of a side-effecting "operation" to a variable name. When you define a monad, you define the set of valid operations (i.e. effects).
All monads need two "special" monadic values: one that represents no operation
(i.e. no side-effect, i.e. a pure computation), usually called "pure" or
"return", and one that represents the effect of chaining together two effects,
usually called "bind" (written >>=
in Haskell, in case you've ever looked
that way).
A monad with just those two is (usually) not very useful, so in general you will add more.
What makes a monad
As promised, no category theory definition. For our purposes, a monad is made out of:
- A data type with multiple variants, amongst which at least
:return
and:bind
. We will refer to values of this type as "monadic values". - A function that can "run" monadic values, with well-defined semantics for
:return
and:bind
, and pretty much arbitrary semantics for all of the other variants. In the rest of this post, we'll refer to this function as "therun
function".
By variant here we mean vectors where the first element is a keyword, and where the number and types of the other elements are defined by that keyword.
The :return
variant has one element of any type; the :bind
variant has two
elements, where the first one is a monadic value and the second one is a
function that takes a "normal" (non-monadic) value and returns a monadic value.
The run
function applied to :bind
must have the semantics of "unwrapping"
the monadic value and applying its function to it.
Because Clojure is a dynamic language, and because the run
function will be
written as a case analysis on the type of its argument(s), we can fully
formalize a specific monad by just specifying its run
function.
A bit of syntax
There is a reason why most monad tutorials use Haskell: without syntactic support, monads are pretty clunky to use. Clojure does not have syntactic support out of the box, but, being a lisp, it has macros. This means we can add syntactic support, in the form of two macros:
(defmacro match
[expr & cases]
(let [e (gensym)]
`(let [~e ~expr]
(case (first ~e)
~@(->> (partition 2 cases)
(mapcat (fn [[pat body]]
[(first pat)
`(let [~(vec (cons '_ (rest pat))) ~e]
~body)])))))))
(defmacro mdo
[bindings]
(if (#{0 1} (count bindings))
(throw
(RuntimeException. "invalid number of elements in mdo bindings"))
(let [[n v & r] bindings]
(if (empty? r)
v
[:bind v `(fn [~n] (mdo ~r))]))))
Why we might want them will be clarified soon enough; for now, let's look at what they do.
First, the match
macro will allow us to choose between a number of
alternatives based on the first element of a vector. Here is an example
expansion:
t.core=> (macroexpand-1 '(match expr
[:lit v] [v env]
[:var idx] [(get env idx) env]
[:set idx e] (let [[v env] (h e env)]
[v (assoc env idx v)])))
(clojure.core/let [G__2018 expr]
(clojure.core/case (clojure.core/first G__2018)
:lit (clojure.core/let [[_ v] G__2018]
[v env])
:var (clojure.core/let [[_ idx] G__2018]
[(get env idx) env])
:set (clojure.core/let [[_ idx e] G__2018]
(let [[v env] (h e env)]
[v (assoc env idx v)]))))
t.core=>
We match on the first element of a vector, and bind the other elements to the symbols we give. In other words, this is a specialized, poor man's core.match. We could of course also just use core.match, but because we don't need its full power here, I don't want to import its full complexity.
This will be very useful in writing the run
function, as the core of what it
does is dispatching on its first argument.
Next, let's take a look at mdo
. Here is an example use:
t.core=> (macroexpand-1 '(mdo [a [:return 5]
b [:return 3]
c [:return (+ a b)]
_ [:set 0 c]
v [:get 2]
_ [:return (+ v c)]]))
[:bind [:return 5] (clojure.core/fn [a]
(t.core/mdo (b [:return 3]
c [:return (+ a b)]
_ [:set 0 c]
v [:get 2]
_ [:return (+ v c)])))]
t.core=>
macroexpand-1
only does one round of macro expansion, so it's not all that
useful for a recursive macro. Unfortunately, a full macroexpand
is not a
whole lot more useful:
t.core=> (macroexpand (mdo [a [:return 5]
b [:return 3]
c [:return (+ a b)]
_ [:set 0 c]
v [:lookup 2]
_ [:return (+ v c)]]))
[:bind
[:return 5]
#object[t.core$eval1756$fn__1757 0x2910aca
"t.core$eval1756$fn__1757@2910aca"]]
t.core=>
So, you'll have to trust me a bit on this one, but here's the result of a manual recursive expansion of the macro:
[:bind [:return 5]
(fn [a]
[:bind [:return 3]
(fn [b]
[:bind [:return (+ a b)]
(fn [c]
[:bind [:set 0 c]
(fn [_]
[:bind [:lookup 2]
(fn [v]
[:return (+ v c)])])])])])]
The semantics we want here is that the run
function will, when running on a
[:bind ma f]
value, first fully evaluate ma
, then run the function f
with
the result of evaluating ma
as the argument. This may be a bit clearer with
an example.
A first example: ambient state
Let's make all of that a little bit more concrete. We start by defining an "ambient state" monad, one in which you can, at any point, reach out to a "global variable" that maintains some piece of state.
Specifically, we want to reify two effects:
[:get k]
will return the value associated to the keyk
.[:set k v]
will set the value associated to keyk
to valuev
.
Here is the definition of this monad, in the form of a run
function:
(defn run-ambient
([m] (run-ambient m {}))
([m env]
(match m
[:return v] [v env]
[:bind ma f] (let [[v env] (run-ambient ma env)]
(run-ambient (f v) env))
[:set k v] [v (assoc env k v)]
[:get k] [(get env k) env])))
There's nothing very complicated about this code: we just keep around some state, in this case a map that represents our ambient state.
Applying that function to our earlier example of a monadic value yields:
t.core=> (run-ambient
(mdo [a [:return 5]
b [:return 3]
c [:return (+ a b)]
_ [:set 0 c]
v [:get 2]
_ [:return (+ v c)]])
{2 3})
[11 {2 3, 0 8}]
t.core=>
That's not very interesting, though. A more realistic use-case would involve calling functions on the right-hand side of these assignments. That would showcase a bit more of the power of this approach: our ambient state would still be available in those subfunctions.
As a more complex example, here is a bit of code that counts, for each type of expression, the number of times it appears in a program, for a simple AST. It also returns the total number of expression nodes in the program.
(defn sequenceM
"Takes a list of monadic values ms, and returns
a single monadic value that wraps a list."
[ms]
(if (empty? ms)
[:return ()]
(mdo [a (first ms)
r (sequenceM (rest ms))
_ [:return (cons a r)]])))
(defn update
[k f]
(mdo [v [:get k]
_ [:set k (f v)]]))
(defn count-exprs
[expr]
(let [finc (fnil inc 0)]
(match expr
[:lit e] (mdo [_ (update :lit finc)
_ [:return 1]])
[:var _] (mdo [_ (update :var finc)
_ [:return 1]])
[:set _ e] (mdo [c (count-exprs e)
_ (update :set finc)
_ [:return (inc c)]])
[:bin _ e1 e2] (mdo [c1 (count-exprs e1)
c2 (count-exprs e2)
_ (update :bin finc)
_ [:return (+ c1 c2 1)]])
[:while e-cond e-body] (mdo [c1 (count-exprs e-cond)
c2 (count-exprs e-body)
_ (update :while finc)
_ [:return (+ c1 c2 1)]])
[:do & exprs] (mdo [counts (sequenceM (mapv count-exprs exprs))
_ (update :do finc)
_ [:return (reduce + 1 counts)]]))))
Note that sequenceM
will work with any monad, while update
is more specific
to this one.
Running this yields:
t.core=> (run-ambient
(count-exprs
[:do
[:set 0 [:lit 100]]
[:set 1 [:lit 1000]]
[:while
[:bin :not= [:lit 0] [:var 1]]
[:do
[:set 0 [:bin :add [:bin :add [:bin :add [:var 0] [:lit 4]]
[:var 0]]
[:lit 3]]]
[:set 0 [:bin :add [:bin :add [:var 0] [:lit 2]]
[:lit 4]]]
[:set 1 [:bin :add [:lit -1] [:var 1]]]]]
[:var 0]]))
[29 {:lit 8, :set 5, :var 6, :bin 7, :do 2, :while 1}]
t.core=>
An interesting note to make about count-exprs
is that there is no explicit
threading of state through the recursive calls. The code reads as imperative,
mutable code, while there is in fact no mutation going on.
Second example: non-deterministic computation
Monads are not just about state management: they can also be used to change the computation model. As a simple example, here is a monad that allows every (non-pure) computation to return multiple results, and runs all the results through the rest of the computation:
(defn run-nd
([ma] (run-nd ma []))
([ma s]
(match ma
[:return a] [a]
[:bind ma f] (mapcat (comp run-nd f) (run-nd ma))
[:multi ls] ls)))
Here is an example run, where we compute all four possible permutations:
t.core=> (run-nd (mdo [a [:multi [1 2]]
b [:multi [3 4]]
_ [:return (+ a b)]]))
(4 5 5 6)
t.core=>
We can make special operators for this monad. For example, we could make a filter that only returns positive values:
(defn filter-pos
[a]
[:multi (if (pos? a) [a] [])])
We can run that filter like this:
t.core=> (run-nd (mdo [a [:multi [-1 2]]
b [:multi [3 4]]
c [:return (* a b)]
d (filter-pos c)
_ [:return d]]))
(6 8)
t.core=>
A simple possible use-case would be the computation of the solution of quadratic equations:
(defn sqrt
[x]
(cond (neg? x) [:multi []]
(zero? x) [:return 0]
(pos? x) (mdo [sign [:multi [1 -1]]
_ [:return (* sign (Math/sqrt x))]])))
(defn div
[a b]
(if (zero? b)
[:multi []]
[:return (/ a b)]))
(defn solve-2nd
[a b c]
(mdo [d (sqrt (- (* b b) (* 4 a c)))
x (div (- d b)
(* 2 a))]))
where we define sqrt
as a function that returns both roots for positive
numbers, and none for negative ones. Similarly, div
will return no solution
for division by 0. This propagates directly to our solver, which will return
zero, one or two answers:
t.core=> (run-nd (solve-2nd 1 2 1))
(-1)
t.core=> (run-nd (solve-2nd 1 3 1))
(-0.3819660112501051 -2.618033988749895)
t.core=> (run-nd (solve-2nd -2 0 -1))
()
Just like there was no explicit threading of state through the body of
count-exprs
, there is no explicit handling of multiple values in the body of
solve-2nd
.
What about category theory?
In order for a monad to be useful, the behaviour of :return
and :bind
need
to follow a set of rules, generally known as "the monad laws" (sometimes
"monadic" laws). In practice, it's pretty hard to violate the laws and still
end up with something useful. Expressed in Clojure (and using the conventions
described in this post), the laws would be:
(and
(= (run [:bind [:return a] f])
(run (f a)))
(= (run [:bind m (fn [x] [:return x])])
(run m))
(= (run [:bind [:bind m g] f])
(run [:bind m (fn [x] [:bind (g x) f])])))
Category theory gives a mathematical justification for those laws. That's it.
Conclusion
This only scratched the surface. Similarly to the first example, you could
define a monad that represents the effect of interacting with your database.
You could then define two run
functions for it: one that actually interacts
with the database, and one, used for testing, that only simulates those
interactions. Your production code would still be the exact same mdo
expression. You could define a monad where the run
function is expected to
receive multiple monadic values, and runs them concurrently, one :bind
step
at a time. This could give you green threads, possible with some
synchronization mechanism à la core.async channels, in surprisingly little
code.
Despite their power, monads are not used very much in Clojure. This is mainly
because we have other, more familiar alternatives. The kind of state management
presented in the first example can easily be achieved through passing around an
atom
, or actually using mutable ambient state in the form of a dynamic
variable. The database use-case can be achieved through passing around an
instance of a protocol, for which you'd have two implementations. Changing the
computational model can be done through macros, as done in core.async and
core.logic.
Ultimately, how much monads seem appealing to you will depend on how much you care about avoiding mutable state. I believe there is a place for mutable state, but there are cases where it's better to avoid it, and knowing about monads gives you one more technique for that.