☕ biscuits in teaProgrammingApplicatives in Pictures↑ Top
Haskell · Programming

Applicatives in Pictures

From fmap to <*>: combining independent values that live inside a shared computational context.

Functors gave us a wonderfully useful operation:

fmap :: (a -> b) -> f a -> f b

Give fmap an ordinary function and a value inside some context, and the function travels into that context.

But now consider a slightly stranger situation. What if the function itself is already inside the context?

Just (+1)   -- a function in Maybe
Just 5      -- a value in Maybe

How do we combine them without opening the boxes by hand?

That is the doorway to Applicative.

TikZ diagram comparing fmap, where a function is outside a context, with applicative application, where both function and value are inside Maybe.
Functor versus Applicative: the crucial new move is allowing the function itself to live inside the context.

The two operations to remember

An Applicative gives us two central operations:

pure  :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b

pure places an ordinary value into the context. <*>, pronounced apply, applies a contextual function to a contextual value.

For Maybe:

pure 5 :: Maybe Int
-- Just 5

Just (+1) <*> Just 5
-- Just 6

Why currying suddenly becomes beautiful

Haskell functions are curried. The type

(+) :: Num a => a -> a -> a

can be read as:

(+) :: a -> (a -> a)

So after supplying one argument, we still have a function waiting for the second argument.

That means we can do this:

pure (+) <*> Just 10 <*> Just 32
-- Just 42

or, more idiomatically:

(+) <$> Just 10 <*> Just 32
-- Just 42

The first <$> is simply fmap written infix.

TikZ diagram showing Just plus, Just 10 and Just 32 combining with applicative operators to produce Just 42.
Applicative application chains naturally because a curried function reveals one argument at a time.
ApplicativeArithmetic.hs
sum3 :: Num a => Maybe a -> Maybe a -> Maybe a -> Maybe a
sum3 x y z = (\a b c -> a + b + c) <$> x <*> y <*> z

example1 = sum3 (Just 10) (Just 20) (Just 12)
-- Just 42

example2 = sum3 (Just 10) Nothing (Just 12)
-- Nothing
One missing input makes the whole Maybe computation unavailable.

Building records without case-analysis soup

Suppose we want to construct a person only when all three fields are present.

data Person = Person
  { name :: String
  , age  :: Int
  , city :: String
  }
  deriving Show

A manual version quickly becomes noisy:

mkPerson :: Maybe String -> Maybe Int -> Maybe String -> Maybe Person
mkPerson mName mAge mCity =
  case mName of
    Nothing   -> Nothing
    Just name ->
      case mAge of
        Nothing  -> Nothing
        Just age ->
          case mCity of
            Nothing   -> Nothing
            Just city -> Just (Person name age city)

Applicative expresses the same structure almost exactly as we would say it:

mkPerson :: Maybe String -> Maybe Int -> Maybe String -> Maybe Person
mkPerson mName mAge mCity =
  Person <$> mName <*> mAge <*> mCity
TikZ diagram showing three independent Maybe fields feeding into a Maybe Person through applicative combination.
Several independent contextual inputs can be assembled into one contextual result.

The list Applicative is delightfully different

With lists, <*> means: apply every function to every value.

[(+1), (*10)] <*> [1, 2, 3]

produces:

[2, 3, 4, 10, 20, 30]

The same interface, <*>, captures a completely different notion of context.

(,) <$> ["tea", "coffee"] <*> ["Marie", "Parle-G"]

becomes:

[ ("tea",    "Marie")
, ("tea",    "Parle-G")
, ("coffee", "Marie")
, ("coffee", "Parle-G")
]

pure is not really about purity

The name can initially mislead. pure does not mean “make this function pure.” It means “take this ordinary value and place it into the minimal context.”

pure 7 :: Maybe Int
-- Just 7

pure 7 :: [Int]
-- [7]

This lets ordinary values participate in Applicative expressions without special machinery.

A useful mental ladder

There is a lovely progression here:

ordinary application
    f x

Functor
    f <$> fx

Applicative
    ff <*> fx

Or, in types:

(a -> b)     -> a   -> b
(a -> b)     -> f a -> f b
f (a -> b)   -> f a -> f b

Each step moves another piece of the computation into the context.

When Applicative is exactly enough

Applicative is excellent when the computations are independent.

For example, parsing three form fields:

User <$> parseName rawName
     <*> parseAge  rawAge
     <*> parseCity rawCity

We already know all three computations before any one of them runs.

But what if the next computation depends on the actual value produced by the previous one?

For instance:

read a user id

use that id to decide which account to fetch

use that account to decide which permissions to load

Now the structure itself is value-dependent.

Applicative cannot express that dependency by itself.

That is precisely where Monad enters.

Exerciseeasy

Predict the result without running GHCi:

(*) <$> Just 6 <*> Just 7

What happens if the second Just 7 is replaced by Nothing?

Reveal solution

The first expression is Just 42. Replacing either required argument with Nothing gives Nothing.

Exercisemedium

Write an Applicative expression that creates a triple from three Maybe values.

The target type is:

triple :: Maybe a -> Maybe b -> Maybe c -> Maybe (a, b, c)
Reveal solution
triple :: Maybe a -> Maybe b -> Maybe c -> Maybe (a, b, c)
triple ma mb mc = (,,) <$> ma <*> mb <*> mc

The tuple constructor (,,) is simply a curried three-argument function.