The option functor

As Tony Hoare once put it, null references were his one billion dollar mistake[3]. Regardless of your background, you will no doubt have encountered versions of the dreadful NullPointerException. This usually happens when we try to call a method on an object reference that is null.

Clojure deals with null values due to its interoperability with Java. In this section we will learn how Clojure provides improved support for dealing with null values.

The core library is packed with functions that do the right thing if passed a nil value (Clojure's version of Java's null). For instance, how many elements are there in a nil sequence? Consider the following code snippet:

(count nil) ;; 0 

Thanks to conscientious design decisions regarding nil, we can, for the most part, afford not to worry about it. For all other cases, the option functor might be of some help.

The remaining examples in this appendix should be in a file called option.clj, under library-design/src/library_design/. You're welcome to try this in the REPL, as well.

Let's start our next example by adding the namespace declaration, as well as the data that we will be working with:

(ns library-design.option 
  (:require [uncomplicate.fluokitten.protocols :as fkp] 
            [uncomplicate.fluokitten.core :as fkc] 
            [uncomplicate.fluokitten.jvm :as fkj] 
            [imminent.core :as i])) 
 
(def pirates [{:name "Jack Sparrow"    :born 1700 :died 1740 :ship "Black Pearl"} 
              {:name "Blackbeard"      :born 1680 :died 1750 :ship "Queen Anne's Revenge"} 
              {:name "Hector Barbossa" :born 1680 :died 1740 :ship nil}]) 
 
(defn pirate-by-name [name] 
  (->> pirates 
       (filter #(= name (:name %))) 
       first)) 
 
(defn age [{:keys [born died]}] 
  (- died born)) 

As a Pirates of the Caribbean fan, I thought it would be interesting to play with pirates in this example. Let's suppose that we would like to calculate Jack Sparrow's age. Given the data and functions that we just covered, this is a simple task:

  (-> (pirate-by-name "Jack Sparrow") 
      age) ;; 40 

However, what if we would like to know Davy Jones' age? We don't actually have any data for this pirate, so if we run our program again, the following is what we'll get:

(-> (pirate-by-name "Davy Jones") 
      age) ;; NullPointerException   clojure.lang.Numbers.ops (Numbers.java:961) 

There it is. The dreadful NullPointerException. This happens because, in the implementation of the age function, we end up trying to subtract two nil values, which is incorrect. As you might have guessed, we will attempt to fix this by using the option functor.

Traditionally, option is implemented as an algebraic data typemore specifically, a sum type with two variants: Some and None. These variants are used to identify whether a value is present, without using nils. You can think of both Some and None as subtypes of option.

In Clojure, we will represent them by using records, as follows:

(defrecord Some [v]) 
 
(defrecord None []) 
 
(defn option [v] 
  (if (nil? v) 
    (None.) 
    (Some. v))) 

As you can see, Some can contain a single value, whereas None contains nothing. It's simply a marker indicating the absence of content. We have also created a helper function, called option, which creates the appropriate record, depending on whether its argument is nil.

The next step is to extend the Functor protocol to both records, as follows:

(extend-protocol fkp/Functor 
  Some 
  (fmap [f g] 
    (Some. (g (:v f)))) 
  None 
  (fmap [_ _] 
    (None.))) 

Here's where the semantic meaning of the option functor becomes apparent—as Some contains a value, its implementation of fmap simply applies the function g
to the value inside of the functor, f, which is of the type Some. Finally, we put the results inside of a new Some record.

Now, what does it mean to map a function over None? You have probably guessed that it doesn't really make sense; the None record holds no values. The only thing that we can do is return another None. As you will see shortly, this gives the option functor a short-circuiting semantic.

In the fmap implementation of None, we could have returned a reference to this, instead of a new record instance. I haven't done so, simply to make it clear that we need to return an instance of None.

Now that we've implemented the functor protocol, we can try it out, as follows:

(->> (option (pirate-by-name "Jack Sparrow")) 
     (fkc/fmap age)) ;; #library_design.option.Some{:v 40} 
 
(->> (option (pirate-by-name "Davy Jones")) 
     (fkc/fmap age)) ;; #library_design.option.None{} 

The first example shouldn't hold any surprises. We convert the pirate map that we get by calling pirate-by-name into option, and then we fmap the age function over it.

The second example is an interesting one. As we stated previously, we have no data about Davy Jones. However, mapping age over it does not throw an exception any longer; instead, it returns None.

This might seem like a small benefit, but the bottom line is that the option functor makes it safe to chain operations together:

(->> (option (pirate-by-name "Jack Sparrow")) 
     (fkc/fmap age) 
     (fkc/fmap inc) 
     (fkc/fmap #(* 2 %))) ;; #library_design.option.Some{:v 82} 
 
(->> (option (pirate-by-name "Davy Jones")) 
     (fkc/fmap age) 
     (fkc/fmap inc) 
     (fkc/fmap #(* 2 %))) ;; #library_design.option.None{} 

At this point, some readers might be thinking about the some-> macro (introduced in Clojure 1.5) and how it effectively achieves the same result as the option functor. This intuition is correct, as demonstrated in the following code snippet:

(some-> (pirate-by-name "Davy Jones") 
        age 
        inc 
        (* 2)) ;; nil 

The some-> macro threads the result of the first expression through the first form if
it is not nil. Then, if the result of that expression isn't nil, it threads it through
the next form, and so on. As soon as any of the expressions evaluates to nil, we see that some-> short-circuits and returns nil immediately.

That being said, a functor is a much more general concept; as long as we are working with this concept, our code doesn't need to change, as we are operating at a higher level of abstraction:

(->> (i/future (pirate-by-name "Jack Sparrow")) 
     (fkc/fmap age) 
     (fkc/fmap inc) 
     (fkc/fmap #(* 2 %))) ;; #<Future@30518bfc: #<Success@39bd662c: 82>> 

In the preceding example, even though we are working with a fundamentally different tool (futures), the preceding code that returns a pirate by name did not have to change. This is only possible because both options and futures are functors, and they implement the same protocol provided by fluokitten. We have gained composability and simplicity, as we can use the same API to work with various different abstractions.

Speaking of composability, this property is guaranteed by the second law of functors. Let's see whether our option functor respects this and the first (identity) laws:

;; Identity 
(= (fkc/fmap identity (option 1)) 
   (identity (option 1))) ;; true 
 
 
;; Composition 
(= (fkc/fmap (comp identity inc) (option 1)) 
   (fkc/fmap identity (fkc/fmap inc (option 1)))) ;; true 

We're done; our option functor is a lawful citizen. The two remaining abstractions also come paired with their own laws. We will not cover the laws in this section, but I encourage you to read about them[4].