Functional Programing with Cats

Should we be using Monads in Clojure?

Functional Human
Jul 5 · 11 min read

In this piece I am going to explore the world of Monads for functional programming. However, aside from going over what the basic Monads are and how to use the Cats library, 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 loose the ability to explain it to others”

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?


The Maybe Monad

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

(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

(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?

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

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

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

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.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade