-
Notifications
You must be signed in to change notification settings - Fork 4
0103: Email Tutorials ‐ Applicative Functors
- Email 600 – Recap: Functors and
fmap - Email 601 – Why Functors Are Not Enough
- Email 602 – The Goal: Mapping Functions with Multiple Arguments
- Email 603 – The “Bad Idea”: A Class for
fmap2,fmap3, … - Email 604 – The Two “Magic” Primitives
- Email 605 – Applicative Style
- Email 606 – Building
fmap0,fmap1, andfmap2frompureand<*> - Email 607 – The
ApplicativeTypeclass in Haskell - Email 608 – Example 1: The
MaybeApplicative - Email 609 – “Exceptional” Programming with
Maybe - Email 610 – Example 2: The List Applicative
- Email 611 – “Nondeterministic” Programming with Lists
- Email 612 – What Applicatives Give You (and What They Don’t)
- Glossary
- Quiz Questions (Table Format)
- Quiz Options (Table Format)
- Next Steps
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 bWhen 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.
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 twoMaybe Intvalues.” - “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.
We can imagine a family of generalized mapping operators:
-
fmap1for 1 argument (this is just normalfmap) -
fmap2for 2 arguments -
fmap3for 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 dThere is also a useful “degenerate” case:
fmap0 :: a -> f aThis “zero-argument mapping” is really just “embedding” a plain value into the structure.
One naive approach would be to define new typeclasses for each level:
-
Functor2forfmap2 -
Functor3forfmap3 - 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.
Instead of defining infinitely many mapping operators, we can define two primitives and build everything from them:
-
pureembeds a value into a structure:
pure :: a -> f a-
(<*>)is a generalized form of function application:
(<*>) :: f (a -> b) -> f a -> f bYou 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.
A common programming pattern with applicatives is:
pure g <*> x <*> y <*> zThis 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)Once you have pure and <*>, you can rebuild the “hierarchy” of mapping operators.
This is just pure:
fmap0 :: a -> f a
fmap0 = pureThis can be defined using applicative style:
fmap1 :: (a -> b) -> f a -> f b
fmap1 g x = pure g <*> xThis works because:
pure g :: f (a -> b)- then
pure g <*> x :: f b
Similarly:
fmap2 :: (a -> b -> c) -> f a -> f b -> f c
fmap2 g x y = pure g <*> x <*> yIn other words, applicatives let you apply a multi-argument pure function to multiple effectful inputs without inventing new operators for each arity.
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 bThis 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.
Maybe represents computations that can fail.
A natural definition of pure for Maybe is:
pure x = Just xThis 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 insidemx.
A standard definition is:
Nothing <*> mx = Nothing
Just g <*> mx = fmap g mxThis uses fmap from the functor instance, because mapping a function over Maybe is exactly “apply if present.”
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 2Example (two arguments):
pure (+) <*> Just 1 <*> Just 2
-- Just 3Failure propagates automatically:
pure (+) <*> Nothing <*> Just 2
-- NothingThe important interpretation is:
Applicative style for
Maybesupports 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.
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.
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.
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).
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.
Bernard Sibanda is a global Technology Entrepreneur, Web3 and Software Consultant with a deep focus on Cardano Blockchain, Midnight and Community building.
Key Positions:
- Founder, CTO, Developer Advocate cohort #1, Fullstake Developer, Cardano Ambassador, Catalyst Project Manager, DREP-WIMS:
- Co-founder of ABL Tech and Cardano Africa Live
- EBU-certified Plutus Pioneer (Plutus/Haskell)
- Cohort #1 Plutus Pioneer Developer
- Catalyst Community Reviewer & Funded Projects Manager
-
DRep for WIMS-Cardano (ID:
drep1yguj8zu48n99pv70yl6ckzt9hdgjy8yjnlqs2uyzcpafnjgu4vkul) - Intersect Developer Advocate
- Intersect Committe Member 2025-2026
- Cardano Marketer,Promoter and blogger
- Cardano Open Source Contributor
- Cardano communities and events organizer and builder
- Cardano Ambassador for South Africa
Official links:
- Stablecoins Dex
- Coxygen Global Universities
- WIMS Cardano Global
- Cardano Africa Live
- WIMS Cardano Videos
- Cardano Smart Contract Videos
- Fullstack IT Consulting
Social links: