More On Applicatives

Last time we looked at functors and started on applicative functors. Now we’re going to expand on applicatives.

Effects

I mentioned that functors were abstractions. Abstractions are used to hide some details while allowing other details to be focused on more easily. Abstractions are useful because they lessen the congnitive load and allow us to build bigger, more complicated software than we would be able to otherwise. So what details do applicatives hide?

Effects are what programs ‘do’. The simplest effect is to call a function and get an answer back. Other effects are changing mutable state, throwing exceptions, writing to a log, or communications with the external world. Often times, these kinds of effects are called ‘side-effects’. Many languages have facilities to deal with side effects built in. In the case where those facilities are missing, they can be implemented using various programming techniques. And usually, these techniques require repetitious code that gets copied from place to place. Applicatives hide this duplicated code that implements various effects.

Juxtapositioning

One kind of side effect is that of having a sequence of functions all read from a common environment. In Clojure, you can do this with the juxt function. And the Reader applicative functor fits the bill. This is just one example where a specific kind of abstraction has been implemented in Clojure in an ad hoc fashion because it’s useful. First off, juxt. In this example :a and :b are being used as functions that extract values from a hash-map:

(def j (juxt :a :b))
(j {:a 3 :b 8})
=> [3 8]

Compare that to the Reader applicative:

(require '[applicative :as a])
(def r (a/map (Reader. vector) :a :b))
(r {:a 3 :b 8})
=> [3 8]

((a/map (Reader. +) :a :b) {:a 3 :b 8})
=> 11

((a/map (Reader. -) :a :b) {:a 3 :b 8))
=> -5

As you can see, the Reader applicative is more flexible than juxt because you can use different functions on the results of the functions you pass in.

Laws

Just as with functors, applicative functors have some laws they must obey. Unfortunately, the way the laws are expressed in Haskell rely on the type system and the fact that a/map is treated as a binary operator, so I’ve not come up with a good way to represent them in Clojure. And since I don’t feel like dropping Haskell code in here and having to explain all the notation, I’m going to gloss over them. You can search for ‘applicative functor laws’ and probably find some good explanations.

Monads

So how do applicatives relate to monads? First off, all monads are also applicative functors, so both are useful for sequencing functions with side effects. Since every monad is also an applicative functor, it is straightforward to implement a/map using return and bind from any monad. However, there will almost certainly be a more effecient way to implement a/map directly.

Also, if you notice, a/map only allows you to execute a series of effectual computations in order. That is, the result of one computation cannot affect the following ones. Since part of the definition of a monad is the existence of bind, and since bind encapsulates the rest of the sequence of computations in a function, the results of an earlier computation can affect later ones.

And since monads are just a special kind of applicative (and functor), bind can be seen as a special kind of map where a function is mapped over the elements of a collection. ‘collection’ being a generic way to refer to a computation.

Epilogue

I’ve a feeling that there’s a lot more to be discovered about applicative functors and how they can be used. The existence of juxt is one indication these abstractions are useful. I’m coming to believe that all programming boils down to managing and composing effects. Applicative functors are one way to do that concisely and I intend to revisit them in the future.

Next up: comonads!

Jim Duey 20 January 2013
blog comments powered by Disqus