Every time I read Learn You a Haskell, I get something new out of it. This most recent time through, I think I’ve finally gained some insight into the
Applicative type class.
I’ve been writing Haskell for some time and have developed an intuition and explanation for
Monad. This is probably because monads are so prevalent in Haskell code that you can’t help but get used to them. I knew that
Applicative was similar but weaker, and that it should be a super class of
Monad but since it arrived later it is not. I now think I have a general understanding of how
Applicative is different, why it’s useful, and I would like to bring anyone else who glossed over
Applicative on the way to
Monad up to speed.
Applicative type class represents applicative functors, so it makes sense to start with a brief description of functors that are not applicative.
Values in a Box
A functor is any container-like type which offers a way to transform a normal function into one that operates on contained values.
Some prefer to think of it like this:
(->) is right-associative, we can reason about and use this function either way – with the former being more useful to the current discussion.
This is the first small step in the ultimate goal between all three of these type classes: allow us to work with values with context (in this case, a container of some sort) as if that context weren’t present at all. We give a normal function to
fmap and it sorts out how to deal with the container, whatever it may be.
Functions in a Box
To say that a functor is “applicative”, we mean that the contained value can be applied. In other words, it’s a function.
An applicative functor is any container-like type which offers a way to transform a contained function into one that can operate on contained values.
Again, we could also think of it like this:
Applicative functors also have a way to take an un-contained function and put it into a container:
In actuality, the type signature is simpler:
a -> f a. Since
a literally means “any type”, it can certainly represent the type
(a -> b) too.
Understanding this is very important for understanding the usefulness of
Applicative. Even though the type signature for
(<*>) starts with
f (a -> b), it can also be used with functions taking any number of arguments.
Consider the following:
(<*>) or not?
Instead of writing its signature with
b, lets use a question mark:
Indeed it is: substitute the type
(b -> c) for every
?, rather than the simple
b in the actual class definition.
One In, One Out
What you just saw was a very concrete example of the benefits of how
(->) works. When we say “a function of n arguments”, we’re actually lying. All functions in Haskell take exactly one argument. Multi-argument functions are really single-argument functions that return other single-argument functions that accept the remaining arguments via the same process.
Using the question mark approach, we see that multi-argument functions are actually of the form:
And it’s entirely legal for that
? to be replaced with
(b -> ?), and for that
? to be replaced with
(c -> ?) and so on ad infinitum. Thus you have the appearance of multi-argument functions.
As is common with Haskell, this results in what appears to be happy coincidence, but is actually the product of developing a language on top of such a consistent mathematical foundation. You’ll notice that after using
(<*>) on a function of more than one argument, the result is not a wrapped result, but another wrapped function – does that sound familiar? Exactly, it’s an applicative functor.
Let me say that again: if you supply a function of more than one argument and a single wrapped value to
(<*>), you end up with another applicative functor which can be given to
(<*>) yet again with another wrapped value to supply the remaining argument to that original function. This can continue as long as the function needs more arguments. Exactly like normal function application.
A “Concrete” Example
Consider what this might look like if you start with a plain old function that (conceptually) takes more than one argument, but the values that it wants to operate on are wrapped in some container.
How do we pass
f to get some overall result? You wrap the function with
pure then use
The first portion of that expression is very interesting:
pure f <*> x. What is this bit doing? It’s taking a normal function and applying it to a contained value. Wait a second, normal functors know how to do that!
Since in Haskell every
Applicative is also a
Functor, that means it could be rewritten equivalently as
fmap f x, turning the whole expression into
fmap f x <*> y.
Never satisfied, Haskell introduced a function called
(<$>) which is just
fmap but infix. With this alias, we can write:
Not only is this epically concise, but it looks exactly like
f x y which is how this code would be written if there were no containers involved. Here we have another, more powerful step towards the goal of writing code that has to deal with some context (in our case, still that container) without actually having to care about that context. You write your function like you normally would, then add
(<*>) between the arguments.
What’s the Point?
With all of this background knowledge, I came to a simple mental model for applicative functors vs monads: Monad is for series where Applicative is for parallel. This has nothing to do with concurrency or evaluation order, this is only a concept I use to judge when a particular abstraction is better suited to the problem at hand.
Let’s walk through a real example.
Building a User
In an application I’m working on, I’m doing OAuth based authentication. My domain has the following (simplified) user type:
During the process of authentication, an OAuth endpoint provides me with some profile data which ultimately comes back as an association list:
Within this list, I can find user data via the
lookup function which takes a key and returns a
Maybe value. I had to write the function that builds a
User out of this list of profile values. I also had to propagate any
Maybe values by returning
First, let’s write this without exploiting the fact that
Maybe is a monad or an applicative:
Maybe as a
Monad makes this much, much cleaner:
Up until a few weeks ago, I would’ve stopped there and been extremely proud of myself and Haskell. Haskell for supplying such a great abstraction for potential failed lookups, and myself for knowing how to use it.
Hopefully, the content of this blog post has made it clear that we can do better.
Series vs Parallel
Monad means we have the ability to access the values returned by earlier
lookup expressions in later ones. That ability is often critical, but not always. In many cases (like here), we do nothing but pass them all as-is to the
User constructor “at once” as a last step.
Applicative, I know this.
-- f :: a -> b -> c -> d User :: Text -> Text -> Text -> User -- x :: f a lookup "first_name" p :: Maybe Text -- y :: f b lookup "last_name" p :: Maybe Text -- z :: f c lookup "email" p :: Maybe Text -- result :: f d -- result = f <$> x <*> y <*> z buildUser :: Profile -> Maybe User buildUser p = User <$> lookup "first_name" p <*> lookup "last_name" p <*> lookup "email" p
And now, I understand when to reach for
Monad. Perhaps you do too?