More Monads
Recently, I answered a question about implementing the IO monad in Clojure. All well and good, except that it took me longer than I would have liked to get back into the swing of reasoning with monads. This is the sort of thing I want to be as natural as, if not breathing, at least working with classes, especially since it's a stepping stone to other concepts like comonads and arrows.
To put myself on firmer ground, I took a day or so to write, from scratch, a couple of monads as well the supporting structure.
We've talked about monads before, but implementing a more general monadic system is interesting. Let's take a look at how it can be done.
Memories Of A Monad
Remember the basics of monads? A monad is defined by a pair of key functions:return
, which wraps a regular value in a monadic context, and bind
, which unwraps a monadic value and passes the regular value inside to a function which produces a new monadic value. We can also define zero
and plus
for extra monadic bling.
So, okay, right. Let's get everybody's head in the game with an example. The sequence monad Seq
is probably the easiest to wrap your head around. A monadic value is a list; a monadic function takes a value and produces a list. It's monad definition is as follows:
(defn return
; given x
[x]
; wrap it in a list
(list x))
(defn bind
; given a list xs
; and a function
[xs f]
; map f over each x in xs
; and combine the result in a single list
(mapcat f xs))
(def zero
; the zero is an empty list
(list))
(def plus
; concat combines lists
concat)
Simple enough, right?
Binging On Binding
Of course, we have a bunch of different types of monads. All of our good friends:Maybe
, Err
, Writer
, and the rest. Each of these has its own definitions for return
, bind
, zero
, and plus
. To avoid clashes, we could differentiate the function names - seq-return
and maybe-return
- but there are some things we want to write for any monad using the generic bind
, return
, and so on without worry about which monad it will actually be used with.
So, there's our problem. We want to write in terms of the abstract return
et al. and define them later.
In something like Haskell, this is would be a nonissue. We just set up a typeclass describing the monad definition and implement it for each monad. Since it's statically typed, the compiler can automatically determine which monad we're trying to use and swap in the correct return
, bind
, and so forth.
Because Clojure is dynamically typed, we don't get that automatic inference. Instead, we'll have to explicitly supply the context. Once we've got a monad, we need to make return
and the rest to use that monad's definitions.
Fortunately, Clojure offers us a handy tool here - binding
. binding
lets us redefine things within its scope. This means we can say, "Hey, I'm using Seq here" and, using binding
, redefine return
and the others to use Seq
's implementations.
Let's write a macro with
which will allow us to set up the monadic context; it'll bind the definitions so we can use them in its body. The monad we pass in will just be a map containing the pieces of its definition.
(defmacro with
[monad & body]
`(let [monad# ~monad]
; set up definitions
(binding [return (:return monad#)
bind (:bind monad#)
zero (:zero monad#)
plus (:plus monad#)]
; execute body in that context
~@body)))
Okay. So, we can define Seq
as a map of definitions:
(def Seq
{:return seq-return
:bind seq-bind
:zero seq-zero
:plus seq-plus})
Then, we can pass it to with
and go about our monad business in the context of Seq
:
(with Seq
(return 1))
; => [1]
(def Better)
To clean up the definition of a monad a little, we can write a quick macro calleddefmonad
. Completely unnecessary, but it alleviates some of the superfluous details.
(defn keywordify-keys
[m]
(into {}
(for [[k v] m]
[(keyword k) v])))
(defmacro defmonad
[name & {:as impl}]
`(def ~name ~(keywordify-keys impl)))
Let's define Seq
with defmonad
. This is identical to the implementation above:
(defmonad Seq
return seq-return
bind seq-bind
zero seq-zero
plus seq-plus)
Do It Up
In my mind, the best feature of Haskell's monads is the do notation. This takes a series of nested binding calls and cleans it up real nice. To make our monads useful, we want to appropriate this syntax. For example, instead of something ugly like this:(with Seq
(bind [1 2 3]
(fn [x]
(bind [:a :b :c]
(fn [y]
[x y])))))
; => ([1 :a] [1 :b] [1 :c]
; [2 :a] [2 :b] [2 :c]
; [3 :a] [3 :b] [3 :c])
We want to be write something beautiful like this:
(with Seq
(<- [x [1 2 3]
y [:a :b :c]]
(return [x y])))
This transformation is actually pretty easy: we just bind a function naming the variable on the left-hand-side of each expression to the right-hand-side.
(defmacro <-
[bindings & body]
(let [bindings (->> bindings
(partition 2)
reverse)]
(reduce
(fn [body [lhs rhs]]
`(bind ~rhs
(fn [~lhs]
~body)))
`(do ~@body)
bindings)))
Classy.
Maybe Some More
At this point, we've got a pretty nice system in place. We've been givingSeq
the spotlight, but let's consider another monad. Maybe
captures possible failures. We either have a value or nothing. Trying to apply any function to nothing will get you more nothing.
In Haskell, the two states of Maybe
are encoded in a datatype: data Maybe a = Nothing | Just a
. We could do something similar with, say, Clojure's record types, but that's not exactly idiomatic. Instead of wrapping stuff up in new types, we'll treat nil
as our Nothing
and everything else as, uh, not nothing:
(defmonad Maybe
return identity
bind (fn [x f]
(when-not (nil? x)
(f x))))
(with Maybe
(<- [x 1
y (inc x)]
(return y)))
; => 2
(with Maybe
(<- [x nil
y (inc x)]
(return y)))
; => nil
Write About It
Another interesting monad isWriter
. Writer
values are of the form [value, message]
. The monadic glue takes care of joining messages together:
(defmonad Writer
return (fn [x]
[x, empty-log])
bind (fn [[x, existing-log] f]
(let [[y, additional-log] (f x)]
[y (append existing-log
additional-log)])))
Okay, cool, but what are empty-log
and append
? Mm. Well. Uh. Yeah.
See, we actually want to be able to configure our Writer
. We could, say, append messages to one long string or, we could add messages to the back of a list.
In Haskell, this is type-system stuff once again. The Writer
type is dependent upon some additional type for the log. What are we going to do in Clojure? Well, hey, a monad is just a map. Let's just pass the Writer
its dependencies dynamically and close over them:
(defn Writer
[{:keys [empty-log append]}]
{:return (fn [x]
[x, empty-log])
:bind (fn [[x, existing-log] f]
(let [[y, additional-log] (f x)]
[y (append existing-log
additional-log)]))})
(with (Writer {:empty-log ""
:append str})
(<- [x [1, "one!"]
y [2, "two!"]]
(return (+ x y))))
; => [3 "one!two!"]
(with (Writer {:empty-log []
:append concat})
(<- [x [1, ["one!"]]
y [2, ["two!"]]]
(return (+ x y))))
; => [3 ["one!"
; "two!"]]
Neat, huh? So, that solves that. We can push our dependencies to runtime with plain ol' functions. That also means you can select those dependencies on the fly. Hell, if you wanted to, you could select the entire monad itself at runtime.
So, that's that. Between functions, macros, and Clojure's core library, we can whip up a decent monad system. Not too shabby. You can check out the whole thing here.