Applicative functors
Like functors, applicative functors are a sort of container, and they define two operations:
(defprotocol Applicative (pure [av v]) (fapply [ag av]))
The pure
function is a generic way to put a value inside of an applicative functor. So far, we have been using the option
helper function for this purpose. We will be using it a little later.
The fapply
function will unwrap the function contained in the applicative ag
, and will apply it to the value contained in the applicative av
.
The purpose of both of the functions will become clear with an example, but first, we need to promote our option
functor to an applicative functor, as follows:
(extend-protocol fkp/Applicative Some (pure [_ v] (Some. v)) (fapply [ag av] (if-let [v (:v av)] (Some. ((:v ag) v)) (None.))) None (pure [_ v] (Some. v)) (fapply [ag av] (None.)))
The implementation of pure
is the simplest. All it does is wrap the value, v
, into
an instance of Some
. Equally simple is the implementation of fapply
for None
.
As there is no value, we simply return None
again.
The fapply
implementation of Some
ensures that both arguments have a value for the :v
keyword; strictly speaking, they both have to be instances of Some
. If :v
is not nil
, it applies the function contained in ag
to v
, finally wrapping the result. Otherwise, it returns None
.
This should be enough to try our first example, using the applicative functor API:
(fkc/fapply (option inc) (option 2)) ;; #library_design.option.Some{:v 3} (fkc/fapply (option nil) (option 2)) ;; #library_design.option.None{}
We are now able to work with functors that contain functions. Additionally, we have also preserved the semantics of what should happen when any of the functors don't have a value.
We can now revisit the average-age example from before, as follows:
(def age-option (comp (partial fkc/fmap age) option pirate-by-name)) (let [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa")] (fkc/<*> (option (fkj/curry avg 3)) a b c)) ;; #library_design.option.Some{:v 56.666668}
Note
The vararg
function, <*>
, is defined by fluokitten
and performs a left-associative fapply
on its arguments. Essentially, it is a convenience function that makes (<*> f g h)
equivalent to (fapply (fapply f g) h)
.
We start by defining a helper function, to avoid repetition. The age-option
function retrieves the age of a pirate for us as an option.
Next, we curry the avg
function to 3
arguments and put it into option
. Then, we use the <*>
function to apply it to the options a
, b
, and c
. We get the same result, but have the applicative functor take care of nil
values for us.
Note
Function currying
: Currying is the technique of transforming a function of multiple arguments into a higher-order function of a single argument that returns more single-argument functions until all of the arguments have been supplied.
Roughly speaking, currying makes the following snippets equivalent:
(def curried-1 (fkj/curry + 2)) (def curried-2 (fn [a] (fn [b] (+ a b)))) ((curried-1 10) 20) ;; 30 ((curried-2 10) 20) ;; 30
Using applicative functors this way is so common that the pattern has been captured as the function alift
, as shown in the following code snippet:
(defn alift "Lifts a n-ary function `f` into a applicative context" [f] (fn [& as] {:pre [(seq as)]} (let [curried (fkj/curry f (count as))] (apply fkc/<*> (fkc/fmap curried (first as)) (rest as)))))
The alift
function is responsible for lifting a function in such a way that it can be used with applicative functors without much ceremony. Because of the assumptions that we are able to make about applicative functors (for instance, that it is also a functor), we can write generic code that can be reused across any applicatives.
With alift
in place, our age average example turns into the following:
(let [a (age-option "Jack Sparrow") b (age-option "Blackbeard") c (age-option "Hector Barbossa")] ((alift avg) a b c)) ;; #library_design.option.Some{:v 56.666668}
We lift avg
into an applicative-compatible version, making the code look remarkably like a simple function application. And, since we are not doing anything interesting with the let
bindings, we can simplify it further, as follows:
((alift avg) (age-option "Jack Sparrow") (age-option "Blackbeard") (age-option "Hector Barbossa")) ;; #library_design.option.Some{:v 56.666668} ((alift avg) (age-option "Jack Sparrow") (age-option "Davy Jones") (age-option "Hector Barbossa")) ;; #library_design.option.None{}
As with functors, we can take the code as it is, and we can simply replace the underlying abstraction, preventing repetition once again:
((alift avg) (i/future (some-> (pirate-by-name "Jack Sparrow") age)) (i/future (some-> (pirate-by-name "Blackbeard") age)) (i/future (some-> (pirate-by-name "Hector Barbossa") age))) ;; #<Future@17b1be96: #<Success@16577601: 56.666668>>