Samuel Evans-Powell
Applicative
Table of Contents
I've been learning about Haskell and Category Theory lately. In this series of posts I hope to collect my thoughts. Please pop me an email or open a pull request here if you want to make any suggestions/corrections, I'm still learning myself.
In my last post, I talked about how a Functor allows you to apply a function
to values within the context of that Functor. In Haskell, this is implemented
by the fmap
function:
fmap :: Functor f => (a -> b) -> f a -> f b
An Applicative Functor is not all that different from a Functor, it still deals with the application of a function to values within the context of a Functor, but where that function is itself in the context of a Functor.
The minimal Applicative typeclass is defined as follows:
class Functor f => Applicative f where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b
As you can see, any data type that is an instance of Applicative must also be an instance of Functor.
Motivating Example
Alright, where are Applicative Functors useful though?
Let's say we have the following data type:
data Address = Address { streetName :: String , streetNum :: Integer , isApartment :: Bool }
To construct an address, we need three things: a String value, an Integer value and a Bool value. The constructing function has the following signature:
Address :: String -> Integer -> Bool -> Address
Now, let's suppose we're parsing these addresses from some address book in a text file and we have the following functions to retrieve the street name, street number and apartment bool from the file:
parseStreetName :: File -> Maybe String parseStreetNum :: File -> Maybe Integer parseIsApartment :: File -> Maybe Bool
Great! So we go ahead and try and parse our address:
parseAddress :: File -> Address parseAddress file = Address (parseStreetName file) (parseStreetNum file) (parseIsApartment file)
Oh no! That doesn't work, the parse
functions return a Maybe type but the
Address
constructor function doesn't take Maybe types. The types don't line
up!
To solve this we could manually inspect the results of each parse function and return a Maybe Address:
parseAddress :: File -> Maybe Address parseAddress file = maybeAddress (parseStreetName file) (parseStreetNum file) (parseIsApartment file) where maybeAddress :: Maybe String -> Maybe Integer -> Maybe Bool -> Maybe Address maybeAddress Nothing _ _ = Nothing maybeAddress _ Nothing _ = Nothing maybeAddress _ _ Nothing = Nothing maybeAddress street num apartment = Just (Address street num apartment)
That's certainly closer to what we want, but it's a little cumbersome to inspect the results of the Maybe each time, surely there's a better way to do this.
Turns out, Applicative Functors can help us here:
parseAddress :: File -> Maybe Address parseAddress file = Address <$> parseStreetName file <*> parseStreetNum file <*> parseIsApartment file
Wow, that was easy! Let's take a moment to step through what's happening here:
According to precedence rules, the above statement happens in the following order:
(((Address <$> parseStreetName file) <*> parseStreetNum file) <*> parseIsApartment file)
<$> (fmap)
Starting from the start:
:: (String -> Integer -> Bool -> Address) -> Maybe String -> Maybe (Integer -> Bool -> Address) (Address <$> parseStreetName file) :: Maybe (Integer -> Bool -> Address)
The Applicative type class necessitates that it's instances also be instances
of the Functor type class. So, Applicative instances can also use fmap
.
All we're doing is 'mapping' the Address
function over the Maybe String
.
Because Maybe is an instance of Functor, it provides the fmap
function
that allows us to do that. However, because not all the arguments of the Address
function are applied, we're left with:
Maybe (Integer -> Bool -> Address)
We'd really like to just use the fmap
function again to map this over the rest
of the results. However now the function itself is in the context of a Maybe
and so the fmap
function is no longer useful. Fortunately, this is exactly
where Applicative Functors come in handy!
<*> (apply)
Remember I said Applicative Functors let us apply functions which are themselves
in the context of a Functor to values which are also in the context of a
Functor? This behaviour is provided by our (<*>)
operator:
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
How will this help us? We had this:
parseAddress :: File -> Maybe Address parseAddress file = Address <$> parseStreetName file <*> parseStreetNum file <*> parseIsApartment file
But we've already resolved the first part:
parseAddress :: File -> Maybe Address parseAddress file = Maybe (Integer -> Bool -> Address) <*> parseStreetNum file <*> parseIsApartment file
Now we need to resolve the next part:
Maybe (Integer -> Bool -> Address) <*> parseStreetNum file
So the signature of the function we're looking for is:
Maybe (Integer -> Bool -> Address) -> Maybe Integer -> Maybe (Bool -> Address)
In other words, it's applying the next argument of our Address constructor.
Starting with the definition of (<*>)
:
(<*>) :: (Applicative f) => f (a -> b) -> f a -> f b
Let's try and match this signature to the signature we require. We'll start by
making the f
a Maybe:
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
And make a
an Integer:
(<*>) :: Maybe (Integer -> b) -> Maybe Integer -> Maybe b
And b
a (Bool -> Address)
:
(<*>) :: Maybe (Integer -> Bool -> Address) -> Maybe Integer -> Maybe (Bool -> Address)
Sweet! Turns out the result of our apply
operator will allow us to apply the
next argument of the function to a value within a Maybe context:
:: Maybe (Integer -> Bool -> Address) -> Maybe Integer -> Maybe (Bool -> Address) ((Address <$> parseStreetName file) <*> parseStreetNum file) :: Maybe (Bool -> Address)
Now all we need to do is use the apply operator again to get our final result:
(<*>) :: Maybe (Bool -> Address) -> Maybe Bool -> Maybe Address
:: Maybe (Bool -> Address) -> Maybe Bool -> Maybe Address (((Address <$> parseStreetName file) <*> parseStreetNum file) <*> parseIsApartment file) :: Maybe Address
It takes a little to understand, but the code that uses Applictive Functors is much less verbose. We use the properties of the Applicative to take care of feeding possible failure from one function application to another. If all parse functions succeed, a Just Address is returned. If any one fails, Nothing is returned.
pure
An Applicative instance must also implement the function 'pure':
pure :: a -> f a
All pure does is 'lift' a value into the context of an Applicative.
For example:
pure :: a -> Maybe a pure x = Just x
pure :: a -> [a] pure x = [x]
Where is this useful? When using an Applicative instance, you may come across situations where you wish to apply a function within a Functor context to a value which is not in that Functor.
For example, you may wish to apply a list of functions to a single value, producing a new list of values, one element for the result of each function being applied to the value:
myFunc :: [(a -> b)] -> a -> [b]
The pure
function allows you to 'lift' a value into the context of a Functor
(in this case a list):
myFunc :: [(a -> b)] -> a -> [b] myFunc fs a = fs <*> pure a
You may also wish to lift a function into the context of a Functor. We did this
in the Address examples using the (<$>)
(fmap) operator, but it could also be
achieved using pure:
parseAddress :: File -> Maybe Address parseAddress file = pure Address <*> parseStreetName file <*> parseStreetNum file <*> parseIsApartment file