Applicative Functors
Welcome to part 2 of our series on monads and other functional structures. We'll continue building our foundation on these ideas by exploring the concept of applicative functors. If you don't yet have a solid grasp on basic functors, make sure to review part 1 of this series! If you think you're ready for monads already, you can move onto part 3!
If you've never written Haskell before, now is the perfect time! Get Haskell installed on your computer by following the instructions from our Beginners Checklist!
You can follow along with all the examples in this part by using GHCI. But you can also take a quick peek at our Monads Github Repository! The ApplicativesComplete module has a few references for Applicative instances we see in this article.
FUNCTORS FALLING SHORT
In part 1, 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)
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 often begin 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.
>> import Control.Applicative
>> ZipList [(1+), (5*), (10*)] <*> [5,10,15]
ZipList {getZipList = [6,50,150]}
CONCLUSION
That wraps up this part on applicative functors. If this all seemed really confusing, don't be afraid to go back to part 1 and make sure you have a solid understanding of normal functors first. If you feel good about your knowledge, you're now ready to move onto part 3 where we finally get down and dirty with monads!
All these concepts are a lot easier to understand if you can try out the code examples for yourself. If you've never programmed in Haskell before, it's not hard to get started! Download our Beginners Checklist to learn how!