Should we avoid Monads in Clojure?

Railway Oriented Programming is one model of functional programming using Monads. Whilst interesting, it seems that ROP is rarely used in code bases. Ivan Grishaev goes into detail as to why he’s not a fan of Monads in Clojure and has a spirited blog piece which is well worth a read:

I will never let monads be in a Clojure project — Ivan Grishaev

Clojure also features the nifty some-> and some->> operators which behave in a similar way to an Either monad. It threads the expression result into each form as long as the result is not nil. If a nil is returned, the entire computation returns nil, so we can short-circuit a computation on any errors.

(some->> val

step1

step2

step3) (some->> {:y 3 :x 5}

:y

(- 2))

; -1 (some->> {:y 3 :x 5}

:z

(- 2))

; nil - :z doesn't exist so the expression short-circuits and returns nil.

This gives us a convenient way to handle errors and write code that is easier to reason about.

The cost of Monads

I’ve recently watched a talk by Rich Hickey called Maybe Not. Rich had some very strong opinions on the use of Monads and highlighted some key issues with this pattern. In some ways, you can think of Monads as a virus infecting your code! Once you introduce them into your program, the virus will quickly spread to other parts of your code base and you will need to use all kinds of mutant versions of map, filter, for, let and other functions just to work with them. Some would argue that they make your code less readable and less composable / generic.

One issue is often overlooked is in code maintenance. Let’s say we have a function that takes an argument x:

; x is a regular clojure value

(defn myfn [x y] x) ; Existing code

(myfn 10 20)

; 10

One of Rich’s arguments against the use of Monad’s is that when we add them to our code base, we can often break things because they are not a first class citizen and do not belong to the type system. Let’s see what happens if we introduce Maybe values into our myfn:

; Updated myfn, x should now be of type Maybe

(defn myfn [x y] (deref x)) ; Existing code

(myfn 10 20) ; class java.lang.Long cannot be cast to class java.util.concurrent.Future

So we just broke our code. We might now need to update all of our calling functions to handle the Maybe values. The same thing happens in reverse, when we decide to take a function that previously returned a Maybe value, and make it return just a plain value. We have to then update any other functions that use the result of this code to now work with the plain value.

The issue here is that Maybe types are not supported by the language. Rich was similarly dismissive of the use of the Either Monad. He argues that many developers use Either as if it were an or, but it’s really nothing like an or. Conceptually it has two branches, left and right. It has no mathematical properties like associativity, commutativity, composition or symmetry.

So how do we go about using partial information in Clojure? Well we have the humble map. Maps are amazing things. They are a super powerful combination of a set and a function. They can take a keyword and return a value, f(keyword) = value. We can also call them with the keywords themselves, e.g. keyword (map) = value and map (keyword) = value.

({:a 1 :b 2} :b) => 2

Maps give us a Maybe-like behavior by default. Imagine our previous function written to use a map instead:

; Design our function to use a map

(defn carmodel [m] (m :model)) ; Call function with model defined

(carmodel {:make "Ford" :model "Fiesta"}) => "Fiesta" ; Call function without model

(carmodel {:make "Ford"}) => nil

Notice how we got Maybe like behavior by just using language primitives?

If x is present, the function (m :model) returns “Fiesta”, if not, (m :model) returns nil. This is similar to Just x and Nothing, but with native code, no external libraries and no complicated lengthy additional structures needed.

The idiomatic way of thinking here is that if x is not there, don’t put the nil key in the map! If we stick to this rule, then we can make our code easier to reason with, as our map will either contain the value or not. Arguably we shouldn’t be putting nil values into maps at all. Remember, a map is a set, and so the set should either contain a value or not. Throwing a nil value in there makes little sense and makes our code more brittle. Another way of looking at it is that if we were to have a map like {:x nil} we now have no idea if x should be there or not. Are we missing a value here? Should we be worried? Is it ok?

This all boils down to place orientated programming, or PLOP as Rich likes to call it. If we explicitly define a slot for our variable x, we have to fill it with something. We could fill it with a nil or even a Maybe value, but it must contain something. Maps allow us to think in a more mathematical fashion and just do away with this idea. We don’t have anything so we simply don’t create a slot for it.

One criticism might be that there is no structure around our arguments, but this can be addressed using the Clojure spec library. It might be tempting to record optionality in our schemas or definitions too. In the example below, we have defined the ::x key to always be required and the ::y key to be optional.

; Don't do this

(s/def ::make string?)

(s/def ::model string?)

(s/def ::carmodel (s/keys :req [::model] :opt [::make]))

This is wrong. Why? It is wrong because the schema has no context. This specification might make sense for one of our functions, but if we decide we need to use the ::carmodel schema somewhere else the context might have changed.

Optionality is always context dependent — Rich Hickey

To fix this, we need to split the optionality away from the schema. The schema should be about the shape of our thing, and we need a selection to define what is required or provided in a context. This will help to make our schemas a lot more reusable.