Skip to content

0103: Email Tutorials ‐ Applicative Functors

Bernard Sibanda edited this page Dec 17, 2025 · 1 revision

AFP 5 — Functors

📚 Table of Contents

  1. Email 600 – Recap: Functors and fmap
  2. Email 601 – Why Functors Are Not Enough
  3. Email 602 – The Goal: Mapping Functions with Multiple Arguments
  4. Email 603 – The “Bad Idea”: A Class for fmap2, fmap3, …
  5. Email 604 – The Two “Magic” Primitives
  6. Email 605 – Applicative Style
  7. Email 606 – Building fmap0, fmap1, and fmap2 from pure and <*>
  8. Email 607 – The Applicative Typeclass in Haskell
  9. Email 608 – Example 1: The Maybe Applicative
  10. Email 609 – “Exceptional” Programming with Maybe
  11. Email 610 – Example 2: The List Applicative
  12. Email 611 – “Nondeterministic” Programming with Lists
  13. Email 612 – What Applicatives Give You (and What They Don’t)
  14. Glossary
  15. Quiz Questions (Table Format)
  16. Quiz Options (Table Format)
  17. Next Steps

Email 600 – Recap: Functors and fmap

Last time, we introduced functors as a generalization of “mapping over lists.” A functor is any container-like type constructor f that supports mapping a function over the values inside it.

The core idea is captured by:

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

When you call fmap g xs, you apply g to the elements inside xs, and you keep the same outer structure. For lists, fmap is essentially the same as map. For Maybe, fmap applies the function only if the value is Just. For trees, fmap applies the function to every leaf.

Email 601 – Why Functors Are Not Enough

A functor lets you apply a function of one argument to values inside a structure. That is powerful, but it has a limitation.

If your function needs two or more arguments, functors alone do not give you a direct “mapping” operator of the right shape.

For example, you might want something like:

  • “Apply + to two Maybe Int values.”
  • “Apply (*) to two lists of integers in a structured way.”
  • “Apply a 3-argument function to three wrapped arguments.”

Functors can map a function over one wrapped argument, but they do not directly explain how to combine multiple wrapped arguments.

Email 602 – The Goal: Mapping Functions with Multiple Arguments

We can imagine a family of generalized mapping operators:

  • fmap1 for 1 argument (this is just normal fmap)
  • fmap2 for 2 arguments
  • fmap3 for 3 arguments
  • and so on…

The types look like this:

fmap1 :: (a -> b) -> f a -> f b
fmap2 :: (a -> b -> c) -> f a -> f b -> f c
fmap3 :: (a -> b -> c -> d) -> f a -> f b -> f c -> f d

There is also a useful “degenerate” case:

fmap0 :: a -> f a

This “zero-argument mapping” is really just “embedding” a plain value into the structure.

Email 603 – The “Bad Idea”: A Class for fmap2, fmap3, …

One naive approach would be to define new typeclasses for each level:

  • Functor2 for fmap2
  • Functor3 for fmap3
  • etc.

This approach is a dead end because it is tedious and arbitrary. You would have to decide an upper limit (3? 10? 100?), and any limit you choose will eventually be too small for someone’s needs.

We want a solution that scales automatically to any number of arguments.

Email 604 – The Two “Magic” Primitives

Instead of defining infinitely many mapping operators, we can define two primitives and build everything from them:

  1. pure embeds a value into a structure:
pure :: a -> f a
  1. (<*>) is a generalized form of function application:
(<*>) :: f (a -> b) -> f a -> f b

You can read <*> as:

“I have a structure full of functions, and a structure full of arguments; apply them in the appropriate structured way.”

This is the key leap: applicatives are functors with a disciplined way to apply wrapped functions to wrapped values.

Email 605 – Applicative Style

A common programming pattern with applicatives is:

pure g <*> x <*> y <*> z

This is called applicative style.

It looks like ordinary function application g x y z, but every argument is inside some context f, such as Maybe, [], or a parser type.

You should also remember that <*> associates to the left, just like normal application:

(((pure g <*> x) <*> y) <*> z)

Email 606 – Building fmap0, fmap1, and fmap2 from pure and <*>

Once you have pure and <*>, you can rebuild the “hierarchy” of mapping operators.

fmap0

This is just pure:

fmap0 :: a -> f a
fmap0 = pure

fmap1

This can be defined using applicative style:

fmap1 :: (a -> b) -> f a -> f b
fmap1 g x = pure g <*> x

This works because:

  • pure g :: f (a -> b)
  • then pure g <*> x :: f b

fmap2

Similarly:

fmap2 :: (a -> b -> c) -> f a -> f b -> f c
fmap2 g x y = pure g <*> x <*> y

In other words, applicatives let you apply a multi-argument pure function to multiple effectful inputs without inventing new operators for each arity.

Email 607 – The Applicative Typeclass in Haskell

In Haskell, applicatives are expressed as a typeclass that extends functors:

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

This means every applicative must already be a functor, and it must define pure and <*>.

The design idea is that fmap handles one structure, while <*> coordinates multiple structures through function application.

Email 608 – Example 1: The Maybe Applicative

Maybe represents computations that can fail.

A natural definition of pure for Maybe is:

pure x = Just x

This makes sense because pure should inject a value into the structure without introducing failure.

For <*>, we want a rule that propagates failure:

  • If the function is missing (Nothing), we cannot apply anything.
  • If we have a function (Just g), we apply it to the argument inside mx.

A standard definition is:

Nothing <*> mx = Nothing
Just g  <*> mx = fmap g mx

This uses fmap from the functor instance, because mapping a function over Maybe is exactly “apply if present.”

Email 609 – “Exceptional” Programming with Maybe

With the Maybe applicative, applicative style gives a clean way to apply pure functions to arguments that may fail.

Example (one argument):

pure (+1) <*> Just 1
-- Just 2

Example (two arguments):

pure (+) <*> Just 1 <*> Just 2
-- Just 3

Failure propagates automatically:

pure (+) <*> Nothing <*> Just 2
-- Nothing

The important interpretation is:

Applicative style for Maybe supports a form of exception-like programming where failure is propagated without explicit case analysis.

You still write pure functions like +, but you can safely combine potentially missing inputs.

Email 610 – Example 2: The List Applicative

Lists represent “multiple possibilities,” so the applicative instance for lists is designed to apply all possible functions to all possible arguments.

A natural pure embeds a value as a singleton list:

pure x = [x]

For <*>, we want all combinations:

gs <*> xs = [ g x | g <- gs, x <- xs ]

This says: choose a function g from the function list, choose an input x from the argument list, and collect every result.

Email 611 – “Nondeterministic” Programming with Lists

With list applicatives, applicative style means:

Apply a pure function to multivalued arguments, producing all possible results.

Example (behaves like mapping when only one list is involved):

pure (+1) <*> [1,2,3]
-- [2,3,4]

Example (two multivalued arguments):

pure (*) <*> [1,2] <*> [3,4]
-- [3,4,6,8]

This is not “pairwise multiplication.” Instead, it is the Cartesian product of possibilities:

  • 1*3, 1*4, 2*3, 2*4

That is why we describe it as nondeterministic programming: each list is a set of choices, and the result includes every combination.

Email 612 – What Applicatives Give You (and What They Don’t)

Applicatives give you a structured and uniform way to combine independent computations.

They are especially good when:

  • you want to combine multiple effectful arguments using a pure function,
  • the structure of the computation is fixed in advance,
  • you do not need later steps to depend on earlier results to decide what to do next.

What applicatives do not give you is full dependent sequencing. If the second step must be chosen based on the actual value produced by the first step, then you typically need monads and (>>=).

So the “journey” is:

Functor (map over one input) → Applicative (combine multiple inputs) → Monad (dependent sequencing).

🧾 Glossary

Functor – a type supporting fmap to map a function over a wrapped value. Applicative functor (Applicative) – a functor that also supports pure and <*>. pure – embeds a normal value into an applicative context. (<*>) – applies a wrapped function to a wrapped value in a structure-preserving way. Applicative style – expressions like pure g <*> x <*> y that resemble normal application. Exceptional programming – using Maybe to propagate failure automatically. Nondeterministic programming – using lists to represent multiple possible results.

📖 Recommended Reading

Buy Book

🎓 Final Step: Quiz & Progress Badge

Start Quiz (Haskell 18 – Functor)

Clone this wiki locally