Functional Programing with Cats

Should we be using Monads in Clojure?

In this piece I am going to explore the world of Monads for functional programming. We will look at what the basic Monads are and how to use the Cats library and I will also be looking at why we perhaps shouldn’t be using them, and should instead be looking at Clojure’s spec library or Clojure’s other language features.

Photo by Raul Varzar on Unsplash

“A person who understands what a monad is instantly lose the ability to explain it to others” — Duglas Crockford

To get started I will focus on the practical application of using some Monads from the excellent Clojure[Script] cats library by Andrey Antukh and Alejandro Gómez:

A little later on, we will also be taking a look at the upcoming features of Clojure’s spec library. You can find out more about spec-alpha2 here.

Why bother?

Most Monad tutorials will argue that we can use Monads to help us stay safe or to handle side-effects. It is indeed a dangerous world out there, and there are lots of things that might break our programs. Monads give us one way to manage uncertainty and to keep errors in check. They can help to provide a safe evaluation of some computation in a context. We can use them to write more generic programs, to manage state and to simplify our code by controlling flow or side-effects. Or at least that is the promise...

In reality, the Clojure language and Spec library also give us a lot of expressive power and we might not actually need them. It is not that they are wrong or bad, it’s just that there are easier ways to achieve the same result without introducing new problems or strange syntax into your code.

The Maybe Monad

This is a very common Monad that you see in multiple languages. Sometimes the Maybe Monad is also called an optional. The Maybe Monad is essentially a pattern for writing code that handles values that may or may not exist. If you adapt your program to work with them then you are less likely to run into an unexpected error because you have made a deliberate choice to work with values that might not exist. This is far better than assuming that a value just exists, especially if you are relying on someone else to provide that value to you or doing anything with asynchronous programming, e.g. from a HTTP request, asynchronous channel or REST API.

If we use the Cats library, we can take advantage of some of these Monad types to work with values that are empty. In cats we call these values Nothing, because they literally represent nothing.

So a Maybe Monad is really like a magical mystery box, it is a container for either Nothing, or Just a value. In some libraries Just might also be called Some.

;(require '[cats.monad.maybe :as maybe])
(maybe/just 1)
(maybe/nothing)
(maybe/just [1 2 3])

The only problem with putting your value in a mystery box, is that many functions aren’t designed to work with mystery boxes. So we often end up using a functional library equivalent. Take the map command in Cats:

(m/fmap inc (maybe/just 1))
;; => (maybe/just 2)

We use m/fmap because we need a modified map function that will work with Maybe values. In this case, fmap is a functor.

A Functor is a special computational context that maps one value to another. So whenever you see the word functor, just think of it as something that describes a mapping from a to b, or from one category to another. It has a precise mathematical definition, but the basic gist is that you can apply a functor to something to change it into something else.

Our fmap functor understands the context of our Maybe Monad, can unwrap its value and apply the function fn (inc) to it, returning another value of the same type (a Maybe Monad in this case).

We can also use fmap with normal Clojure data types:

(m/fmap inc [1 2 3])
;; => [ 2 3 4]

We can also deref our special Maybe Monad as follows to get its value:

(deref (maybe/just 1))
;; => 1

(deref (maybe/nothing))
;; => nil

But what if we want to extract the value from one Maybe Monad and place it into another Maybe Monad, i.e. preserve the type?

(maybe/from-maybe (maybe/just 1))
;; => 1

(maybe/from-maybe (maybe/nothing))
;; => nil

Applicative Functors

Remember how we said that a Functor was basically something that describes how to map a to b? Well now we can introduce the idea of an Applicative Functor, i.e. something that is applicative and applies a mapping from a to b whilst understanding the context. The important point to emphasize is that the applicative functor understands the context of its operations. Most Monad tutorials will take a function, say:

(def add3 (partial + 3))

Normally we could use add3 on a value:

(add3 3)
;; => 6

But what happens if we wrapped our add3 functor in a context as well?

(maybe/just add3)

Well we can no longer use the value as normal because it has added context that Clojure just won’t understand.

((maybe/just add3) (maybe/just 3))
;; Error

So we need to use some special applicative functors that can handle these:

(m/fapply (maybe/just add3) (maybe/just 3))

We can also use fmap to map applicatives:

(m/fmap (maybe/just add3) (maybe/just [1 2 3]))
;; => [4 5 6]

Applicative functors give us a way to avoid null checks. They can do this because they understand the context that the computation is being performed in and return the same type. Take this example from the Cats documentation:

(defn make-greeter
[^String lang]
(condp = lang
"es" (fn [name] (str "Hola " name))
"en" (fn [name] (str "Hello " name))
nil))

This function can be supercharged to handle null values as follows:

(defn make-greeter
[^String lang]
(condp = lang
"es" (just (fn [name] (str "Hola " name)))
"en" (just (fn [name] (str "Hello " name)))
(nothing)))

All we have done is to turn make-greeter into something that can handle null values. If we pass in a null value to make-greeter, it simply returns nothing.

(fapply (make-greeter "es") (just "Alex"))
;; => #<Just "Hola Alex">

(fapply (make-greeter "en") (just "Alex"))
;; => #<Just "Hello Alex">

(fapply (make-greeter "it") (just "Alex"))
;; => #<Nothing>

We have eliminated all null checks and yet our program still works as expected, returning Nothing if the language isn’t recognized.

The Either Monad

The Either Monad allows us to return the result of a computation which returns a value or some failure. In many languages we often will return the left value if something failed or right value if we successfully completed the computation, so Either Monad’s are great for error handling.

(require '[cats.monad.either :refer :all])(right :valid-value)
;; => #<Right [:valid-value :right]>
(left "Error message")
;; => #<Either [Error message :left]>

Either Monads can be very useful when chained together, as if the computation succeeds the result of the computation can be passed right into the next item, but if it fails or throws an error, the computation will stop and no further steps will be processed. You can think of this as calling right if the value is correct, i.e. right, and left if the computation result is not right.

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.

; Instead, always separate optionality from the schema; Schema "The shape of our thing"
(s/def ::make string?)
(s/def ::model string?)
(s/def ::car (s/schema [::make ::model]))
; Selection (:: model is required for::car-model spec)
(s/def ::car-model (s/select ::car [::model])
(s/valid ::car-model {model: "Fiesta"}) => true

The big picture

I think the following slide is important as it gives us an approach we can use to think about defining specifications. It breaks specifications down into attributes, schemas and selections. If we keep optionality and context specific requirements in our selections, we can make our schemas more resuable and easier to extend.

This is how we should think about specifications and composition — Rich Hickey: Maybe Not

Key points:

  • Schemas describe things that flow together
  • Schemas should need no context
  • Selections describe a particular set of requirements and relate to a context
  • Function parameters might make more sense as a selection because there might be a specific usage context in mind.
  • We always want to keep optionality away from schemas and use selections instead to define what fields we need.

Conclusion

It seems that there are some strong opinions on whether or not Monads are actually needed in Clojure, as we can use a lot of the built in language features to replicate monadic behavior and it avoids making our code more complicated than it needs to be. Indeed, some parts of Clojure such as the go macro are built using monadic functions to keep track of state.

I highly recommend Rich Hickey’s Maybe Not talk from the December Clojure Conj 2018. Spec alpha2 is taking shape nicely and it looks like a powerful approach to writing specifications that can easily evolve over time. Rich has borrowed a lot of ideas from the W3C Resource Description Framework (RDF) as many of these ontology and specification problems have been solved previously.

Functional Programming for Humans

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store