Applicative Functors
What if we had 2 (or more) parallel railways and want to merge them? For example, by using head
, we can easily retrieve the elements of the list and combine them together in whatever manner we wish:
head (+)
[Int] -------> Int ━━━┓
┣━━━ Int
[Int] -------> Int ━━━┛
head
x, y, z :: Int
x = head [1, 2, 3]
y = head [4, 5, 6]
z = x + y -- 5
However, when we are using head'
, combining them is not so easy!
head' ???
[Int] -------> Maybe Int ━━━┓
┣━━━ ???
[Int] -------> Maybe Int ━━━┛
head'
x, y :: Maybe Int
x = head' [1, 2, 3]
y = head' [4, 5, 6]
z = x + y -- ???
As a first attempt, let us try mapping (+)
onto x
:
x, y :: Maybe Int
x = head' [1, 2, 3]
y = head' [4, 5, 6]
f :: Maybe (Int -> Int)
f = fmap (+) x
The question now is, how do we apply f :: Maybe (Int -> Int)
above onto y :: Maybe Int
?
Applicatives
If a functor f
has the ability to apply f (a -> b)
onto a f a
to give an f b
, then it is an applicative functor, which has the same laws of a (lax-) closed (lax-) monoidal functor in category theory. Although we could give the formal definition of these, it is quite a lot to unpack, and not necessary for understanding how to use them. Instead, let us directly show the Applicative
typeclass and some laws that govern these typeclass methods.
class Functor f => Applicative f where
-- pure computation in context
pure :: a -> f a
-- function application in context
(<*>) :: f (a -> b) -> f a -> f b
These methods are subject to:
- Identity:
pure id <*> v
=v
- Homomorphism:
pure f <*> pure x
=pure (f x)
- Interchange:
u <*> pure y
=pure ($ y) <*> u
- Composition:
pure (.) <*> u <*> v <*> w
=u <*> (v <*> w)
The four laws above, again, govern Applicatives
to behave in the obvious way. However, as we shall see, there is more than one obvious way, therefore, whenever you're using instances of Functor
s, Applicative
s and some of the other typeclasses, ensure you read their documentation to understand which obvious way it behaves.
Let us look at an example Applicative
instance:
instance Applicative Maybe where
pure :: a -> Maybe a
pure = Just
(<*>) :: Maybe (a -> b) -> Maybe a -> Maybe b
Nothing <*> _ = Nothing
_ <*> Nothing = Nothing
Just f <*> Just x = Just $ f x
As you can see, pure
just raises a value into the Maybe
context using the Just
constructor, and (<*>)
applies a function in context onto an argument in context when they exist. In other words, pure
and <*>
behave in the most obvious way.
With this in mind, let us show how we can use pure
and <*>
for Maybe
, but also, applicatives in general. Suppose we have f :: a -> b -> c
, x :: a
and y :: b
. Then, f x y
would give us something of type c
.
However, Let us raise x
and y
into the Maybe
context, i.e. x :: Maybe a
and y :: Maybe b
. Let's see how we can perform the same application (similar to f x y
) to give us something of Maybe c
.
To start, we know that we have <*>
which applies a function in context with an argument in context. Therefore, we first raise f
into the Maybe
context using pure
, then apply it onto x
using <*>
:
pure f :: Maybe (a -> b -> c)
pure f <*> x :: Maybe (b -> c)
Finally, using <*>
again allows us to apply the resulting function onto y
, giving us a result of type Maybe c
:
pure f <*> x <*> y :: Maybe c
pure f
Maybe a --<*>--> ━━━┓
┣━━━━ Maybe c
Maybe b --<*>--> ━━━┛
However, recall from our very first example that we had attempted to use fmap
to apply (+)
onto a Maybe Int
to give a Maybe (Int -> Int)
. Now we know that we can directly use this result and apply it onto another Maybe Int
to give us a Maybe Int
, thereby applying (+)
in context! This is a natural consequence of the applicative laws, where pure f <*> x
is the same as fmap f x
!
pure f <*> x == Just f <*> x
== case x of
Just y -> Just $ f y
Nothing -> Nothing
== fmap f x
Therefore, Haskell also defines a function <$>
as an alias of fmap
:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
Therefore, instead of using pure f <*> x
, we can just write fmap f x
or f <$> x
to achieve the same effect!
pure f <*> x <*> y
= fmap f x <*> y
= f <$> x <*> y
Now let us revisit our earlier example again! Here is a naive approach to applying (+)
onto x
and y
:
x, y, z :: Maybe Int
x = head' [1, 2, 3]
y = head' [4, 5, 6]
z = case (x, y) of
(Just x, Just y) -> x + y
_ -> Nothing
Don't torture yourself! Instead, knowing that Maybe
is an applicative (and therefore also a functor), let us just use <$>
and <*>
!
x, y, z :: Maybe Int
x = head' [1, 2, 3]
y = head' [4, 5, 6]
z = (+) <$> x <*> y -- Just 5
As you can see, Applicative
s allow us to perform computation in context separately, and apply a function over the results over these terms in context!
So far, you should have noticed that the functions and typeclasses presented perform the usual stuff, but in context:
fmap :: (a -> b) -> f a -> f b
: lifts a function into a function in contextpure :: a -> f a
: puts pure computation in context<*> :: f (a -> b) -> f a -> f b
: function application in context
With these, here are some guidelines for when to use fmap
, pure
and <*>
:
f x
becomesfmap f x
orf <$> x
orpure f <*> x
ifx
becomes in contextf x
becomesf <*> x
if bothf
andx
become in contextf x y z
becomesf <$> x <*> y <*> z
ifx
,y
andz
become in context