You might have noticed that all reactive abstractions we have encountered in this book have a few things in common. For one, they work as "container-like" abstractions:
Then, once we have this "container," we can operate on it in a number of ways, which are very similar across the different abstractions and frameworks: we can filter
the values contained in them, transform them using map
, combine abstractions of the same type using bind
/flatMap
/selectMany
, execute multiple computations in parallel, aggregate the results using sequence
, and much more.
As such, even though the abstractions and their underlying workings are fundamentally different, it still feels they belong to some type of higher-level abstractions.
In this appendix, we will explore what these higher-level abstractions are, the relationship between them, and how we can take advantage of them in our projects.
We will get started by taking a look at one of the most used operations in these abstractions: map
.
We've been using map
for a long time in order to transform sequences. Thus, instead of creating a new function name for each new abstraction, library designers simply abstract the map
operation over its own container type.
Imagine the mess we would end up in if we had functions such as transform-observable
, transform-channel
, combine-futures
, and so on.
Thankfully, this is not the case. The semantics of map
are well understood to the point that even if a developer hasn't used a specific library before, he will almost always assume that map
will apply a function to the value(s) contained within whatever abstraction the library provides.
Let's look at three examples we encountered in this book. We will create a new leiningen project in which to experiment with the contents of this appendix:
$ lein new library-design
Next, let's add a few dependencies to our project.clj
file:
... :dependencies [[org.clojure/clojure "1.6.0"] [com.leonardoborges/imminent "0.1.0"] [com.netflix.rxjava/rxjava-clojure "0.20.7"] [org.clojure/core.async "0.1.346.0-17112a-alpha"] [uncomplicate/fluokitten "0.3.0"]] ...
Don't worry about the last dependency—we'll get to it later on.
Now, start an REPL session so that we can follow along:
$ lein repl
Then, enter the following into your REPL:
(require '[imminent.core :as i] '[rx.lang.clojure.core :as rx] '[clojure.core.async :as async]) (def repl-out *out*) (defn prn-to-repl [& args] (binding [*out* repl-out] (apply prn args))) (-> (i/const-future 31) (i/map #(* % 2)) (i/on-success #(prn-to-repl (str "Value: " %)))) (as-> (rx/return 31) obs (rx/map #(* % 2) obs) (rx/subscribe obs #(prn-to-repl (str "Value: " %)))) (def c (chan)) (def mapped-c (async/map< #(* % 2) c)) (async/go (async/>! c 31)) (async/go (prn-to-repl (str "Value: " (async/<! mapped-c)))) "Value: 62" "Value: 62" "Value: 62"
The three examples—using imminent, RxClojure, and core.async, respectively—look remarkably similar. They all follow a simple recipe:
As expected, this outputs the value 62
three times to the screen.
It would seem map
performs the same abstract steps in all three cases: it applies the provided function, puts the resulting value in a fresh new container, and returns it. We could continue generalizing, but we would just be rediscovering an abstraction that already exists: Functors.
Functors are the first abstraction we will look at and they are rather simple: they define a single operation called fmap
. In Clojure, Functors can be represented using protocols and are used for containers that can be mapped over. Such containers include, but are not limited to, lists, Futures, Observables, and channels.
The Algebra in the title of this Appendix refers to Abstract Algebra, a branch of Mathematics that studies algebraic structures. An algebraic structure is, to put it simply, a set with one or more operations defined on it.
As an example, consider Semigroups, which is one such algebraic structure. It is defined to be a set of elements together with an operation that combines any two elements of this set. Therefore, the set of positive integers together with the addition operation form a Semigroup.
Another tool used for studying algebraic structures is called Category Theory, of which Functors are part of.
We won't delve too much into the theory behind all this, as there are plenty of books [9][10] available on the subject. It was, however, a necessary detour to explain the title used in this appendix.
Does this mean all of these abstractions implement a Functor protocol? Unfortunately, this is not the case. As Clojure is a dynamic language and it didn't have protocols built in—they were added in version 1.2 of the language—these frameworks tend to implement their own version of the map
function, which doesn't belong to any protocol in particular.
The only exception is imminent, which implements the protocols included in fluokitten
, a Clojure library providing concepts from Category theory such as Functors.
This is a simplified version of the Functor protocol found in fluokitten
:
(defprotocol Functor (fmap [fv g]))
As mentioned previously, Functors define a single operation. fmap
applies the function g
to whatever value is inside the container, Functor, fv
.
However, implementing this protocol does not guarantee that we have actually implemented a Functor. This is because, in addition to implementing the protocol, Functors are also required to obey a couple of laws, which we will examine briefly.
The identity law is as follows:
(= (fmap a-functor identity) (identity a-functor))
The preceding code is all we need to verify this law. It simply says that mapping the identity
function over a-functor
is the same as simply applying the identity
function to the Functor itself.
The composition law is as follows:
(= (fmap a-functor (comp f g)) (fmap (fmap a-functor g) f))
The composition law, in turn, says that if we compose two arbitrary functions f
and g
, take the resulting function and apply that to a-functor
, that is the same as mapping g
over the Functor and then mapping f
over the resulting Functor.
No amount of text will be able to replace practical examples, so we will implement our own Functor, which we will call Option
. We will then revisit the laws to ensure we have respected them.
As Tony Hoare once put it, null references are his one billion dollar mistake (http://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare). Regardless of background, you no doubt will 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 embraces null values due to its interoperability with Java, its host language, but it provides improved support for dealing with them.
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?
(count nil) ;; 0
Thanks to conscious design decisions regarding nil
, we can, for the most part, afford not worry about it. For all other cases, the Option
Functor might be of some help.
The remaining of the 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 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 for this example. Let's say we would like to calculate Jack Sparrow's age. Given the data and functions 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, this 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 type, more specifically a sum type with two variants: Some
and None
. These variants are used to identify whether a value is present or not without using nils
. You can think of both Some
and None
as subtypes of Option
.
In Clojure, we will represent them using records:
(defrecord Some [v]) (defrecord None []) (defn option [v] (if v (Some. v) (None.)))
As we 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
or not.
The next step is to extend the Functor
protocol to both records:
(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 the Functor f
, which is of type Some
. Finally, we put the result inside a new Some
record.
Now what does it mean to map a function over a None
? You probably guessed that it doesn't really make sense—the None
record holds no values. The only thing we can do is return another None
. As we will see shortly, this gives the Option
Functor a short-circuiting semantic.
Now that we've implemented the Functor protocol, we can try it out:
(->> (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 we get from calling pirate-by-name
into an option, and then fmap
the age function over it.
The second example is the interesting one. As stated previously, we have no data about Davy Jones. However, mapping age
over it does not throw an exception any longer, instead returning 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 as follows:
(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, some->
short-circuits and returns nil immediately.
That being said, Functor is a much more general concept, so 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 code using the result did not have to change. This is only possible because both Options and futures are Functors and 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 if our Option Functor respects this and the first—the 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
And we're done, our Option
Functor is a lawful citizen. The remaining two abstractions also come paired with their own laws. We will not cover the laws in this section, but I encourage the reader to read about them (http://www.leonardoborges.com/writings/2012/11/30/monads-in-small-bites-part-i-functors/).
18.225.235.144