Applicative Functors
This lecture is adjusted from Niki Vazou who adjusted from Ranjit Jhala who adjusted from Graham Hutton.
Mapping Functions of Multiple Arguments
Thus far we have only dealt with mapping of single argument functions. However, there is no reason we can’t map a multiple argument function. For example, what do you think will be the type of x
after the following is executed?
Prelude> x = fmap (+) [1,2]
Recall that Haskell has currying, meaning that we can partially apply the two argument function +
with one argument.
Prelude> :t ((+) 1) ((+) 1) :: Num a => a -> a
Using this knowledge, for x
, we will get something that looks like this
x = [(+ 1),(+ 2)]
Indeed, if we check the type of x
, ghci tells us the type of x
is a list of functions from a
to a
where a
is an instance of the Num
typeclass:
Prelude> :t x x :: Num a => [a -> a]
Motivating Applicative Functors
Let’s now consider a slightly different example. Let’s say we have two lists
Prelude> x = [1..5] Prelude> y = [101..105]
As discussed previously, let’s think of lists as representing nondeterminism. That is to say, x
represents a single value that could be any number 1 to 5, y
101 to 105. Let’s suppose we want to add these two nondeterministic values. We could try to use fmap
again.
Prelude> fmap (fmap (+) x) y <interactive>:31:7: error: • Couldn't match expected type ‘Integer -> b’ with actual type ‘[Integer -> Integer]’ • Possible cause: ‘fmap’ is applied to too many arguments In the first argument of ‘fmap’, namely ‘(fmap (+) x)’ In the expression: fmap (fmap (+) x) y In an equation for ‘it’: it = fmap (fmap (+) x) y • Relevant bindings include it :: [b] (bound at <interactive>:31:1)
Why didn’t this work? Well, let’s take a look at the types.
Prelude> :t fmap fmap :: Functor f => (a -> b) -> (f a -> f b) Prelude> :t (fmap (+) x) (fmap (+) x) :: (Enum a, Num a) => [a -> a]
Hmmm. fmap
expected a function from a
to b
but we gave it a function from list a
to b
. Hmmm, how can we get around this? Let’s take a step back and look at what fmap
does, per it’s type signature. We can think of fmap
as both 1) taking a function from a
s to b
s and an f a
and returning an f b
and 2) taking a function from a
s to b
s and returning a function from f a
s to f b
s. In this sense, we can think of fmap
as lifting a function from a
s to b
s to the context (functor) f a
s to f b
s.
Ok, well, can we write a function that would lift a function from a
s to b
s to c
s (like +
) to a particular functor context (like lists)? Let’s try to write such a function.
lift2 :: (a -> b -> c) -> [a] -> [b] -> [c] lift2 f xs ys = [ f x y | x <- xs, y <- ys]
Let’s see if it works.
Prelude> lift2 (+) x y [102,103,104,105,106,103,104,105,106,107,104, 105,106,107,108,105,106,107,108,109,106,107, 108,109,110]
Good, this performs our nondeterministic addition! But what about lifting a function that takes three arguments? Four? N? This seems like a place where currying might be nice.
Let’s define a typeclass to handle this.
class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b
The Applicative
typeclass has two functions pure
and <*>
. pure
takes a type a
and turns it into an Applicative
(and a Functor
). We can think of pure
as turning a
into a default instance of the Applicative
typeclass.
For <*>
, given an applicative function from a
to b
and an applicative a
, we get back an applicative b
.
Note first that for a type f
to be an instance of the Applicative
typeclass, it must also be an instance of the Functor
typeclass, meaning it has a defined fmap
function.
Why is this? It turns our every applicative is also a functor! How can we prove this? Well, if we can derive fmap
from our two functions, pure
and <*>
, we’re good. Let’s do that.
fmap' :: Applicative a => (b -> c) -> a b -> a c fmap' f x = pure f <*> x
Let’s look at the List instance of Applicative
.
instance Applicative [] where pure x = [x] fs <*> xs = [f x | f <- fs, x <- xs]
Notice pure
returns the singleton list as opposed to the empty list. While both would satisfy the type requirements of pure
, the empty list would ignore the argument x
to pure
which would be unusual (Note: while unusual, Haskell would allow this definition of pure
).
Ok, this is nice, but how do we get lift2
from this?
lift2' :: Applicative a => (b -> c -> d) -> (a b -> a c -> a d) lift2' f x y = (pure f) <*> x <*> y
And it works!
Prelude> (lift2' (+) x y) == (lift2 (+) x y) True
Ah-ha! What about lift3
?
lift3' :: Applicative a => (b -> c -> d -> e) -> (a b -> a c -> a d -> a e) lift3' f x y z = (pure f) <*> x <*> y <*> z
Nifty!
Applicative Maybe
Let’s look at the Maybe
instance of the Applicative
typeclass.
instance Applicative Maybe where pure = Just Nothing <*> _ = Nothing (Just g) <*> mx = fmap g mx
Let’s take a look at some examples to get a feel for how this works.
Prelude> pure (+1) <*> Just 1 Just 2 Prelude> pure (+) <*> Just 1 <*> Just 2 Just 3 Prelude> pure (+) <*> Nothing <*> Just 2 Nothing
Recall that we often use Nothing to represent failure/exception. That means the use of applicatives here can represent the propagation of exceptions -- error handling!
Let’s look at an example. Let’s first define a function div’
that divides two integers, returning the result, unless the denominator is zero, in which case it throws an error.
divv x y = if y == 0 then error "Denominator is 0!" else x / y
Here are some examples.
Prelude> divv 1 2 0.5 Prelude> divv 1 0 *** Exception: Denominator is 0! CallStack (from HasCallStack): error, called at <interactive>:30:31 in interactive:Ghci13
Hmmm, maybe we can use applicatives to define a safer division operator.
safeDivv x y = if y == 0 then Nothing else Just (x / y)
Now instead of crashing on divide by zero, we get Nothing.
Prelude> safeDivv 1 2 Just 0.5 Prelude> safeDivv 1 0 Nothing
Let’s try to do some arithmetic with our new safe division function.
Prelude> (safeDivv 1 2) + (safeDivv 4 0) <interactive>:73:1: error: • Non type-variable argument in the constraint: Num (Maybe a) (Use FlexibleContexts to permit this) • When checking the inferred type it :: forall a. (Num (Maybe a), Eq a, Fractional a) => Maybe a
Well that’s no good. Can we somehow make plus work for our new safe division without defining a whole new safePlus
function? Sure, with applicatives!
Prelude> pure (+) <*> (safeDivv 1 2) <*> (safeDivv 4 0) Nothing
Woah, cool! With applicatives we get safePlus
for free! And safeTimes
too!
The Applicative Laws
Similar to functors, applicatives must also follow some laws, in this case four.
Identity:
pure id <*> v = v
Composition:
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
Identity:
pure f <*> pure x = pure (f x)
Identity:
u <*> pure y = pure ($ y) <*> u
I would not spend too long trying to digest these; they can be a bit challenging.
Exercises for Home
Can you define the applicative instance for the Either typeclass?