Updated

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 Functors, Applicatives 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, Applicatives 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 context
  • pure :: 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 becomes fmap f x or f <$> x or pure f <*> x if x becomes in context
  • f x becomes f <*> x if both f and x become in context
  • f x y z becomes f <$> x <*> y <*> z if x, y and z become in context