Our last abstraction will solve the very problem that we raised in the previous section—how to safely perform intermediate calculations by preserving the semantics of the abstractions that we're working with (in this case, options).
It should be no surprise by now that fluokitten also provides a protocol for monads, simplified and shown as follows:
(defprotocol Monad (bind [mv g]))
If you think in terms of a class hierarchy, monads would be at the bottom of it, inheriting from applicative functors, which, in turn, inherit from functors. That is, if you're working with a monad, you can assume that it is also an applicative and a functor.
The bind function of monads takes a function, g, as its second argument. This function receives as input the value contained in mv, and returns another monad containing its result. This is a crucial part of the contract—g has to return a monad.
The reason why will become clearer after a number of examples. But first, let's promote our option abstraction to a monad; at this point, option is already an applicative functor and a functor:
(extend-protocol fkp/Monad Some (bind [mv g] (g (:v mv))) None (bind [_ _] (None.)))
The implementation is fairly simple. In the None version, we can't really do anything; so, just like we have been doing so far, we return an instance of None.
The Some implementation extracts the value from the monad mv and applies the function g to it. Note that this time, we don't need to wrap the result, as the function g already returns a monad instance.
Using the monad API, we could sum the ages of our pirates as follows:
(def opt-ctx (None.)) (fkc/bind (age-option "Jack Sparrow") (fn [a] (fkc/bind (age-option "Blackbeard") (fn [b] (fkc/bind (age-option "Hector Barbossa") (fn [c] (fkc/pure opt-ctx (+ a b c)))))))) ;; #library_design.option.Some{:v 170.0}
First, we make use of the applicative's pure function in the innermost function. Remember that the role of pure is to provide a generic way to put a value into an applicative functor. Since monads are also applicative, we make use of them here.
However, since Clojure is a dynamically typed language, we need to hint pure with a type of context we wish to use. This context is simply an instance of either Some or None. They both have the same pure implementation.
While we do get the right answer, the preceding example is far from what we would like to write, due to its excessive nesting. It is also hard to read.
Thankfully, fluokitten provides a much better way to write monadic code, called the do-notation, as follows:
(fkc/mdo [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa")] (fkc/pure opt-ctx (+ a b c))) ;; #library_design.option.Some{:v 170.0}
Suddenly, the same code becomes a lot cleaner and easier to read, without losing any of the semantics of the option monad. This is because mdo is a macro that expands to the code equivalent of the nested version, as we can verify by expanding the macro, as follows:
(require '[clojure.walk :as w]) (w/macroexpand-all '(fkc/mdo [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa")] (option (+ a b c)))) ;; (uncomplicate.fluokitten.core/bind ;; (age-option "Jack Sparrow") ;; (fn* ;; ([a] ;; (uncomplicate.fluokitten.core/bind ;; (age-option "Blackbeard") ;; (fn* ;; ([b] ;; (uncomplicate.fluokitten.core/bind ;; (age-option "Hector Barbossa") ;; (fn* ([c] (fkc/pure opt-ctx (+ a b c)))))))))))
As an example, when Haskell added do-notation to the language, a new version of the compiler was released, and developers wishing to use the new feature had to upgrade. In Clojure, on the other hand, this new feature can be shipped as a library, due to the power and flexibility of macros. This is exactly what fluokitten has done.
Now, we are ready to go back to our original problem—gathering stats about the pirates' ages.
First, we will define a couple of helper functions that will convert the result of our stats functions into the option monad:
(def avg-opt (comp option avg)) (def median-opt (comp option median)) (def std-dev-opt (comp option std-dev))
Here, we take advantage of function composition in order to create monadic versions of existing functions.
Next, we will rewrite our solution by using the monadic do-notation that we learned earlier:
(fkc/mdo [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa") avg (avg-opt a b c) median (median-opt a b c) std-dev (std-dev-opt a b c)] (option {:avg avg :median median :std-dev std-dev})) ;; #library_design.option.Some{:v {:avg 56.666668, ;; :median 60, ;; :std-dev 12.472191289246473}}
This time, we were able to write the function as we normally would, without having to worry about whether any values in the intermediate computations are empty. This semantic (that is the very essence of the option monad) is still preserved, and can be seen as follows:
(fkc/mdo [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa") avg (avg-opt a b c) median (median-opt a b c) std-dev (std-dev-opt a b c)] (fkc/pure opt-ctx {:avg avg :median median :std-dev std-dev})) ;; #library_design.option.None{}
For the sake of completeness, we will use futures to demonstrate how the do-notation works for any monad:
(def avg-fut (comp i/future-call avg)) (def median-fut (comp i/future-call median)) (def std-dev-fut (comp i/future-call std-dev)) (fkc/mdo [a (i/future (some-> (pirate-by-name "Jack Sparrow") age)) b (i/future (some-> (pirate-by-name "Blackbeard") age)) c (i/future (some-> (pirate-by-name "Hector Barbossa") age)) avg (avg-fut a b c) median (median-fut a b c) std-dev (std-dev-fut a b c)] (i/const-future {:avg avg :median median :std-dev std-dev})) ;; #<Future@3fd0b0d0: #<Success@1e08486b: {:avg 56.666668, ;; :median 60, ;; :std-dev 12.472191289246473}>>