Applicatives: One Step Further
So last week, we discussed the Functor typeclass. We found it allows us to run transformations on data regardless of how the data is wrapped. No matter if our data were in a List, a Maybe
, an Either
, or even a custom type, we could simply call fmap
. However, what happens when we try to combine wrapped data? For instance, if we try to have GHCI interpret these calculations, we’ll get type errors:
>> (Just 4) * (Just 5)
>> Nothing * (Just 2)
Functors Falling Short
Can functors help us here? We can use fmap to wrap multiplication by the particular wrapped Maybe
value:
>> let f = (*) <$> (Just 4)
>> :t f
f :: Num a => Maybe (a -> a)
>> (*) <$> Nothing
Nothing
This gives us a partial function wrapped in a Maybe
. But we still cannot unwrap this and apply it to (Just 5)
in a generic fashion. So we have to resort to code specific to the Maybe
type:
funcMaybe :: Maybe (a -> b) -> Maybe a -> Maybe b
funcMaybe Nothing _ = Nothing
funcMaybe (Just f) val = f <$> val
This obviously won’t work with other functors types.
Applicatives to the Rescue
This is exactly what the Applicative typeclass is for. It has two main functions:
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
The pure function takes some value and wraps it in a minimal context. The <*>
function, called sequential application, takes two parameters. First, it takes a function wrapped in the context. Next, a wrapped value. Its output is the result of applying the function to the value, rewrapped in the context. An instance is called an applicative functor because it allows us to apply a wrapped function. Since sequential application takes a wrapped function, we typically begin our use of applicatives by wrapping something with either pure or fmap. This will become more clear with some examples.
Let’s first consider multiplying Maybe values. If we are multiply by a constant value we can use the functor approach. But we can also use the applicative approach by wrapping the constant function in pure
and then using sequential application:
>> (4 *) <$> (Just 5)
Just 20
>> (4 *) <$> Nothing
Nothing
>> pure (4 *) <*> (Just 5)
Just 20
>> pure (4 *) <*> Nothing
Nothing
Now if we want to multiply 2 maybe values, we start by wrapping the bare multiplication function in pure
. Then we sequentially apply both Maybe
values:
>> pure (*) <*> (Just 4) <*> (Just 5)
Just 20
>> pure (*) <*> Nothing <*> (Just 5)
Nothing
>> pure (*) <*> (Just 4) <*> Nothing
Nothing
Implementing Applicatives
From these examples, we can tell the Applicative
instance for Maybe
is implemented exactly how we would expect. The pure
function simply wraps a value with Just
. Then to chain things together, if either the function or the value is Nothing
, we output Nothing
. Otherwise apply the function to the value and re-wrap with Just
.
instance Applicative Maybe where
pure = Just
(<*>) Nothing _ = Nothing
(<*>) _ Nothing = Nothing
(<*>) (Just f) (Just x) = Just (f x)
The Applicative
instance for Lists is a little more interesting. It doesn’t exactly give the behavior we might first expect.
instance Applicative [] where
pure a = [a]
fs <*> xs = [f x | f <- fs, x <- xs]
The pure
function is what we expect. We take a value and wrap it as a singleton in a list. When we chain operations, we now take a LIST of functions. We might expect to apply each function to the value in the corresponding position. However, what actually happens is we apply every function in the first list to every value in the second list. When we have only one function, this results in familiar mapping behavior. But when we have multiple functions, we see the difference:
>> pure (4 *) <*> [1,2,3]
[4,8,12]
>> [(1+), (5*), (10*)] <*> [1,2,3]
[2,3,4,5,10,15,10,20,30]
This makes it easy to do certain operations, like finding every pairwise product of two lists:
>> pure (*) <*> [1,2,3] <*> [10,20,30]
[10,20,30,20,40,60,30,60,90]
You might be wondering how we might do parallel application of functions. For instance, we might want to use the second list example above, but have the result be [2,10,30]
. There is a construct for this, called ZipList
! It is a newtype around list, whose Applicative
instance is designed to use this behavior.
>> ZipList [(1+), (5*), (10*)] <*> [5,10,15]
ZipList {getZipList = [6,50,150]}
Summary
- Applicative functors take the idea of normal functors one step further.
- They allow function application to occur within the wrapper context.
- In some circumstances, this allows us to reuse generic code.
- In other cases, this gives us clean idioms to express simple concepts and solve common problems.