Who in Their Right Mind Would Use Monads in Clojure?
It is a well known fact that any respectable programmer with a blog must write a tutorial on monads. And it must start with functors and build its way up to monads. I will let respectable programmers worry about this. This is not a tutorial on monads.
This is not about how to use monads in Clojure either. Also this is not some static typing versus dynamic typing comparison/flamewar. I think they both have strengths and weaknesses and neither of them is superior to the other in all areas. I reserve the right to change my opinion in favor of statically typed languages in the future.
TL;DR; You might find monads helpful in expressing large computations even in a dynamic context.
Although I will try to keep it simple, some Haskell knowledge is necessary to better understand this post.
Monads in Hiding
Clojure already takes advantage of monadic operations implicitly. Since Clojure is a dynamically typed language, it doesn’t have explicit type annotations. Since it doesn’t have type annotations, there is no way to check type safety at compile time. Because we can’t statically check types, Clojure is called a dynamically typed language. Static versus dynamic typing have little to do with monads. But if you have a purely functional and statically typed language you would find that you are reaching out to monads[1] to complete the puzzle[2].
Dynamic languages have more free-form abstractions readily available. For a lot of people Python is the first language these days. And without getting a degree in Computer Science they can use sequences and lambda calculus effectively. Similarly when writing Clojure code sticking with a few basic data types allows us to understand and integrate code. We don’t think too much about whether they are semigroups or not. What do I mean by hidden monads though?
class Monad m where
return :: a -> m a
(>>=) :: m a -> (a -> m b) -> m b
What do monads do when they are not shielding functional programmers from other programming models? Above are the two basic operations monads provide; return and bind (i.e. >>=).
return wraps some value in a monadic context. In other words it builds a monad from a value. Just like in these examples:
(def seq-m [1 2 3]) ;; a sequence monad
(def read-m (fn [x] (+ 42 x))) ;; a reader monad
We didn’t use return or any other special function to make these. It doesn’t make them behave any less monadic though. See how we bind them without using bind:
(for [x [1 2 3 4]
y ['a 'b 'c]]
[x y]) ;; -> [[1 'a] [1 'b] [1 'c] [2 'a] ... [4 'c]]
They even compose:
(comp #(* % %) #(+ % 7)) ;; (fn [x] (let [x' (+ x 7)] (* x' x')))
Of course there is not a single monad in sight here. A function is a function, a vector is a vector. But they behave like a monad when they are used in certain Clojure operations. This is what I mean by a hidden monad.
I would like to give one more example before we move on:
(def result (some-> input
(do-this)
(then-that)
(continue-with-this-one)
(one-more)
(lastly-this)))
This is equivalent to:
-- All functions are defined as a -> Maybe a
-- input has a type of a
result = do
r1 <- doThis input
r2 <- thenThat r1
r3 <- continueWithThisOne r2
r4 <- oneMore r3
r5 <- lastlyThis
return r5
-- result has a type of a, whatever a is...
Both snippets take the input and call the functions in order. If any of them return Nothing (Haskell) or nil (Clojure) the calculation stops and the result would be Nothing / nil.
No Types No Problem
Why aren’t there explicitly expressed monads in core Clojure? Without type checking and purity it we don’t necessarily need it. I mentioned this above but let me expand on it a bit more.
First let’s take a look at purity. Clojure encourages keeping as much as your code pure and isolating side effecting parts. But this doesn’t change the fact that any function call may have side effects. There is no telling, other than the author’s assurance or reading the source, that a function is pure. Haskell on the other hand is allows only[3] pure functions and keeps side effects in the context of monads such as IO. You write your code as if there won’t ever be side effects, yet you know and control side effects by manipulating IO or State etc. So having monads as explicit entities in a purely functional language is kind of a necessity. It has to be monads or something else to represent side effects. In Clojure there are no guarantees of purity, some side effecting call can happen anywhere in the call tree and monads wouldn’t save you. Case in point:
(swap! some-atom
(fn [current-value]
(println current-value)
(inc current-value)))
Type checkers, combined with an IDE running them as you type in code, provide instant feedback on your code. They are helpful design tools. Dynamic languages can’t take advantage of this[4]. I have found TDD to be very helpful. I enjoy the type checker’s feedback when I am doing Scala development and I don’t miss it when I am writing Clojure code. But I digress. The point I am trying to make is monads or algebraic data types to be precise make static typing possible and not the other way around[5]. It is not a question of whether you can have monads in a dynamically typed language then. The question is; what good are monads if we don’t have purity or static type guarantees?
Homemade Bind
I was working on this project that used these two templates to signal success and failure:
{:result :success
...}
{:result :failure
:error ...}
This isn’t a distributed real-time high-availability big-data cloud enterprise massively scalable eventually ignorant project, so the computations were running on the same process occasionally fanning out to multiple threads but mostly in a sequential manner. So this success/failure abstraction was internal. And a failure was being converted to an ExceptionInfo and thrown at the boundary of 3rd party code calling ours.
I was manipulating these in two ways. First was to match[6] on :result:
(match [result]
[{:result :success
:x x} (do-something-with x)
[{:result :failure
:error error}] (throw (ex-info "aarrrgh! get to the choppa!" error)))
Second thing was to return values and throw ExceptionInfo’s with the error data attached, then convert the ExceptionInfo to a map with [:result :failure]. Only to be converted back to an ExceptionInfo again in the end. I was doing this to short circuit calculations. Both methods can be acceptable in isolation when applied to small pieces of code. I knew it was getting unwieldy, but I postponed refactoring. I postponed it until I found myself thinking of writing a method that would propagate :failures as they are but apply a given function in case of a :success.
I realized using proper monads would be better for maintainability and readability. I replaced my :success / :failure data structure with either monad from cats library. So now I could write code like this:
(if (did-we-fail-or-what?)
(either/left {:things :about, :the :error})
(either/right {:x 3 :y 4}))
;; This is like some->
(->> some-input
;; Like reduce, it will short-circuit on first error.
(m/foldl process-each (either/right initial-value))
;; fmap is a no-op for errors.
(m/fmap final-thing)
;; to avoid one right inside another.
(m/bind really-final-thing-this-time))
;; 3rd party code doesn't have to use cats.
(either/branch result-m
(fn [error] (throw (ex-info "choppa, get to the" error)))
identity)
;; Generic stuff, use whatever monad. For
;; certain specific values of whatever.
(m/bind some-monad-m
(fn [value]
(let [new-value (f value)]
;; return will return the same type of monad as some-monad-m
(m/return new-value))))
It might not apparent be from the examples above, but using monads (only either monad for now) allowed me to decouple individual computation steps with the code about their composition. Most of the code looks like this now:
(defn f [v]
(-> v
(g)
(h)))
Here v is not a monad, but possibly some value that used to be contained in one. g returns a plain value. h either returns a monad, or it also returns a plain value like g. If h returns a monad, we invoke f with bind, if it returns a value we invoke f with fmap. But this is not a concern of f, it doesn’t know anything about monads.
I don’t have any definitive conclusions for this post. In a way it’s all comparing apples to oranges. But I will still list my points below and if you have any objections or corrections or if you agree with any of this please feel free to write.
- Monads are less useful in a dynamically typed environment.
- Monads can help make composition of computations more explicit. With the advantage of being quite generic abstractions.
- Monads are possible in Clojure. But they are not essential.
Thanks for reading this 1600+ words long rant.
[1] | And functors, applicatives, arrows, etc. |
[2] | Since you have read LYAH, you should already know that. |
[3] | Well, mostly. |
[4] | When you use core.typed your code is no longer dynamically typed. |
[5] | I should add in the absence of inheritance. But Haskell doesn’t have the kind of inheritance Java or Python has. And programming languages with OO style inheritance are not pure. |
[6] | clojure.core.match/match |
If you have any questions, suggestions or corrections feel free to drop me a line.