The next generally useful type class we'll be looking at is Applicative
. Applicative
is built on top of Functor
. For that reason, the Applicative
instances can only be defined for types that have a Functor
instance.
Let's take a look at the definition of Applicative
.
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
First, it provides a function called pure
which given any a
, outputs an f a
. So all pure
does is provide a way to take some simple value and embed it into our structure. We'll see what that means in a moment.
The two angle brackets with the asterisk in the middle is pronounced "apply" or "app" and sometimes by other names.
What apply does is take some function that is embedded in a structure and apply to a value inside of some structure.
Let's go back to Functor for a minute. Functor let us take ordinary functions and sort of promote it to work within some context or structure. As an example, let's specialize the map
operation from Functor to the Optional type. It looks like this:
map :: (a -> b) -> Optional a -> Optional b
map
takes a plain function from a
to b
and an Optional a
and applies the function within that context, outputting an Optional b
. We can state this in a much clearer and concise way by adding some parentheses like this:
map :: (a -> b) -> (Optional a -> Optional b)
You should remember from our discussion on currying and how every function really outputs another function, that these additional parentheses make no difference - the function is exactly the same with or without them. map
really takes a function from a
to b
and outputs a function from Optional a
to Optional b
. Now we can see it more clearly, what Functor
provides is a way to promote an ordinary function to a function that works within some computational context. The Optional
type is a computational context. We can use map
to take a function from a
to b
and turn that into a function from an Optional a
to Optional b
. The same applies for any f
where f
is some type of structure.
Ok, so far so good. Functor lets us apply a function within some context. But what about applying a function which itself exists within some structure to a value inside some structure. In other words, what if we have Optional (a -> b)
and Optional a
? How can apply the function inside of this Optional
context to the value in the other Optional
? Functor will not help us with this.
Functor gives us:
map :: (a -> b) -> f a -> f b
but what we're looking for is
apply :: f (a -> b) -> f a -> f b
We can line these up to see clearly how Functor and Applicative relate to one another. Let's remove the name and just line up the types.
(a -> b) -> f a -> f b
f (a -> b) -> f a -> f b
They are exactly the same except that in the case of Applicative
the function that we want to apply is itself embedded within some context. That's the f
over here in the first argument.
Let's start with a dead simple type, we'll call it Id
and we'll write both a Functor
and Applicative
instance for it.
data Id a = Id a deriving Show
This is not a very useful type, but for our examples all we want is a type that just adds some context for a value. Instead of a plain a
, we have an Id a
.
The Functor
instance for this is trivial. Let's write it:
instance Functor Id where
map f (Id a) = Id (f a)
All we have to do is apply the function to the a
value and wrap the resulting value back up using the Id
constructor.
And now we can use the Functor instance for Id
the same way we use it for any other type.
> map (+ 1) (Id 1)
Id 2
What about the Applicative
instance. Well Applicative
requires us to define both the pure
function and the apply
function. Let's try pure
first.
instance Applicative Id where
pure a = Id a
Implementing pure
is very simple. There's only one possible thing we could do which is to just output an Id
value by shoving the a
inside of one using the Id
constructor.
Now let's implement apply
:
Id f <*> Id a = Id (f a)
This implementation is also pretty easy in the case of Id
. We have a function inside of the Id
context and a value of type a
in an Id
context. So we just grab the function, apply it to the a
and wrap it back up in Id
.
Let's try it out:
> Id (+ 1) <*> Id 1
Id 2
Here we have a function that adds 1 to an Int
. We use apply
from Applicative
to apply this function to the value 1 in the other Id
.
Let's take a detour for a moment to help us develop a bit of a clearer understanding of Applicative
.
We know that in Haskell, function application has higher precedence than any other operator. For that reason, the following won't work:
> let greeting = "hello"
> putStrLn greeting ++ " world"
<interactive>:6:1:
Couldn't match expected type ‘[Char]’ with actual type ‘IO ()’
In the first argument of ‘(++)’, namely ‘putStrLn greeting’
In the expression: putStrLn greeting ++ " world"
We'd like to print "hello world" but this doesn't work because of the precedence of function application. The high precedence means that Haskell tries parsing what we wrote like this:
(putStrLn greeting) ++ " world"
That's exactly what the error said, even if it said it in a pretty confusing way. The error told is that the ++
operator expects a String, but in this case it found a type of IO ()
on the left side. That's because it parsed it the way we just showed.
How can we fix it. We can use parantheses as we've been doing from time to time.
> putStrLn (greeting ++ " world")
hello world
There's another way we can fix this which is by using an alternative function application operator. The operator is represented be a dollar sign and has the following type signature:
($) :: (a -> b) -> a -> b’
As you can see, this operator really doesn't do anything. It just takes a function from a
to b
and an a and outputs a b
. But isn't that just function application which we can accomplish with simple whitespace as we've been doing all along? This is correct! But what this operator adds is the fact that it has the lowest precedence of any operator. The result is that when you have a dollar sign, it's like saying "please evaluate everything to the right of me first". Our "hello world" example is a good one to illustrate use of the dollar sign:
> putStrLn $ greeting ++ " world"
hello world
We use the dollar sign to force the concatenation of "hello" and "world" first. putStrLn is then applied to this entire evaluated expression instead of binding to "hello" first.
Remember this dollar sign operator because it is used frequently in Haskell code. Style and other considerations will determine when to use dollar signs versus parentheses. You'll get a better sense of it the more code you read and write.
Why did I bring up the dollar sign operator here? Because it's a very useful operator to compare with our apply
operator from Applicative
. Let's line them up and take a look.
(a -> b) -> a -> b
f (a -> b) -> f a -> f b
Lining these operators up this way gives us a very easy way to think about Applicative
. apply
is exactly the same as the dollar sign operator, but with everything inside of an f
context. What this means is that we can look as apply
from Applicative
as nothing more than function application within some computational context! So apply
is precisely function application with the only difference being that everything in apply
is wrapped in an f
context.
Exercises
1
Implement the Applicative
instance for the Optional
type.
2
Implement the Applicative
instance for our List
type.
There are actually two different and completely legitimate ways we could implement the Applicative
instance for List
. Since the f
in this case is List
, what we'll have is:
List (a -> b) -> List a -> List b
Since our "context" is List
we have many functions and many elements a
to apply those functions to. As a result, we could define the <*>
operator so that it applies each function to each element of the List
. If we do that, it means the output will be a list whose length is the length of the list of functions multiplied by the list of a
s.
The other way we could define it is by pairing up the functions in the list and the elements a
in the other list. The first function will be applied to the first element of the List a
, the second function to the second element of List a
and so on and so forth.
We can only have a single Applicative
instance though so pick one and implement that. We will see later how to achieve having Applicative
defined for List
in two different ways.