Maybe as an Applicative Functor

Maybe type is made an instance of the Applicative type class as follows:

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> something = fmap f something

It took me a several attempts before I could parse this definition. I was particularly puzzled by the line (Just f) <*> something = fmap f something. To help me understand this better, I decided to build this definition back up from the very basics.

First, let’s look at the Functor type class definition:

class Functor f where
    fmap :: (a -> b) -> f a -> f b

This means that for a given functor type f (i.e. type that is an instance of the Functor type class), fmap takes a function from a -> b and a functor (box) that contains a and returns a functor (box) that contains b. An intuitive way to think about this is that fmap opens the box containing a and applies the function a -> b to it, which results in b.

Now let’s see how Maybe is an instance of the Functor type class:

instance Functor Maybe where
    fmap func Nothing  = Nothing
    fmap func (Just x) = Just (func x)

Now, let’s look at the definition of the Applicative type class:

class (Functor f) => Applicative f where  -- 1
    pure :: a -> f a                      -- 2
    (<*>) :: f (a -> b) -> f a -> f b     -- 3

For a type f that is an instance of the Applicative type class, here is what each line means:

  1. f must also be a functor (i.e. be an instance of the Functor type class).
  2. The pure function takes an arbitrary type a and brings it into the functor. i.e. pure puts a in a box of type f.
  3. <*> takes a functor (box) of type f that contains a function of type a -> b, and a functor (box) of type f that contains type a. It results in a functor (box) of type f that contains b.

With the preamble out of the way, let’s make Maybe an instance of the Applicative type class. To do that, I need to implement the pure and <*> methods for the Maybe type. Below is a line-by-line implementation:

instance Applicative Maybe where
    pure x = Just x

For the Maybe type, pure simply wraps an arbitrary type in Just, thus making it a Maybe value. E.g. writing pure 4 :: Maybe Int in GHCi results in Just 4.

    (<*>) Nothing _ = Nothing

Here, Nothing maps to f (a -> b) from the Applicative class definition. We cannot extract a function out of Nothing, so the result will be Nothing regardless of the second argument.

    (<*>) (Just func) Nothing = Nothing

In the line above, (Just func) maps to f (a -> b), and Nothing maps to f a from the class definition. <*> extracts func out of Just func, and applies it to Nothing. Applying a function to Nothing results in Nothing (or, using the box analogy, applying a function to an empty box results in an empty box)

    (<*>) (Just func) (Just x) = Just (func x)

(Just func) maps to f (a -> b), and Just x maps to f a from the Applicative class definition. <*> extracts the function from Just func, and applies it to x inside the Just x box. The result is Just (func x).

Now, let’s put the definition of <*> for Maybe type next to the definition of the fmap function:

    (<*>) (Just func) Nothing  = Nothing
    (<*>) (Just func) (Just x) = Just (func x)

    fmap func Nothing  = Nothing
    fmap func (Just x) = Just (func x)

This makes it obvious that in the definition of <*>, once we extract func out of Just func, we simply map that function over the second argument of <*> (which will be of Maybe type as well). This means that the <*> implementation for Maybe can be re-written as:

    <*> (Just func) something = fmap func something

This is exactly how the <*> function is implemented at the beginning of this blog post.

Finally, Functors, Applicatives, And Monads In Pictures by Aditya Bhargava is one of the best posts I’ve read on functors & applicatives. I highly recommend it.