Lecture 22 Applicatives

Joseph Haugh

University of New Mexico

The Limitation of Functor

Recall Functor:

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

fmap is great for applying a single-argument function inside a context:

ghci> fmap (+1) (Just 5)
Just 6

ghci> fmap (*2) [1, 2, 3]
[2, 4, 6]

But what if our function takes two arguments?

Two Arguments in a Context

Suppose we have:

ghci> fmap (+) (Just 3)

What does this give us?

ghci> fmap (+) (Just 3)
Just (3+) -- this would really be a Show error

We now have a Maybe (Int -> Int) – a function inside a context.

Can we apply it to Just 5?

Two Arguments in a Context

ghci> fmap (+) (Just 3)
Just (3+) 

fmap cannot help us here:

fmap :: (a -> b) -> f a -> f b
--       ^^^^^^
--  This must be a plain function, not one wrapped in Maybe!

We need a new operation that can apply a function inside a context to a value inside a context.

What We Need

We want:

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

Compare with fmap:

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

The only difference is that in (<*>), the function is itself wrapped inside the structure.

Applicative

Haskell provides the Applicative typeclass:

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

Notice the superclass constraint: every Applicative is also a Functor.

Two operations:

  • pure: lifts a plain value into the structure
  • (<*>): applies a function-inside-structure to a value-inside-structure

Applicative: Maybe

instance Applicative Maybe where
    pure x = Just x

    Nothing <*> _       = Nothing
    _       <*> Nothing = Nothing
    Just f  <*> Just x  = Just (f x)

If either side is Nothing, the result is Nothing.

If both are Just, we apply the function.

Applicative Maybe: Leverage Functor

Since we have a Functor constraint we can also write it like this:

instance Applicative Maybe where
    pure x = Just x

    Nothing <*> _ = Nothing
    Just f  <*> m = fmap f m

Applicative Maybe: In Action

instance Applicative Maybe where
    pure x = Just x

    Nothing <*> _ = Nothing
    Just f  <*> m = fmap f m
ghci> pure (+3) <*> Just 5
Just 8

ghci> pure (+3) <*> Nothing
Nothing

ghci> Nothing <*> Just 5
Nothing

Chaining With <*>

The real power comes from chaining multiple (<*>) calls to handle functions of two or more arguments:

ghci> pure (+) <*> Just 3 <*> Just 5
Just 8

Let’s trace through this step by step.

Chaining With <*>: Trace {data-auto-animate=}

pure (+) <*> Just 3 <*> Just 5
| { pure wraps (+) in a Just }
Just (+) <*> Just 3 <*> Just 5
| { Just (+) <*> Just 3 applies (+) to 3 }
Just (3+) <*> Just 5
| { Just (3+) <*> Just 5 applies (3+) to 5 }
Just 8

Chaining With <*>: Missing Values

If any value in the chain is Nothing, the whole result is Nothing:

ghci> pure (+) <*> Nothing <*> Just 5
Nothing

ghci> pure (+) <*> Just 3 <*> Nothing
Nothing

This models computations that can fail: if any step fails, the whole computation fails.

The <$> Operator

Haskell provides (<$>) as an infix alias for fmap:

(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap

This lets us write a very clean pattern. Compare:

-- Using pure and <*>
pure f <*> x <*> y

-- Using <$> and <*>
f <$> x <*> y

The second reads almost like a normal function application f x y, but operating inside a context.

<$> and <*>: Examples

ghci> (+) <$> Just 3 <*> Just 5
Just 8

ghci> (+) <$> Nothing <*> Just 5
Nothing

Three arguments:

f :: Int -> Int -> Int -> Int
f x y z = x + y * z

ghci> f <$> Just 2 <*> Just 3 <*> Just 4
Just 14

The pattern f <$> x <> y <> z applies f to values inside the structure, carrying along whatever “effect” the structure models.

Applicative: Lists

instance Applicative [] where
    pure x    = [x]
    fs <*> xs = [f x | f <- fs, x <- xs]
  • pure wraps a value in a single-element list
  • (<*>) produces every possible pairing of function and argument

Applicative Lists: In Action

instance Applicative [] where
    pure x    = [x]
    fs <*> xs = [f x | f <- fs, x <- xs]
ghci> [(+1), (*2)] <*> [10, 20, 30]
[11, 21, 31, 20, 40, 60]

Each function is applied to each value:

(+1) 10 = 11    (+ 1) 20 = 21    (+1) 30 = 31
(*2) 10 = 20    (* 2) 20 = 40    (*2) 30 = 60

List Applicative: Combinations

The list applicative naturally expresses all combinations:

ghci> (,) <$> [1, 2, 3] <*> ['a', 'b']
[(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]

This is equivalent to a list comprehension:

[(x, y) | x <- [1, 2, 3], y <- ['a', 'b']]

List Applicative: Combinations

ghci> (,) <$> [1, 2, 3] <*> ['a', 'b']
[(1,'a'),(1,'b'),(2,'a'),(2,'b'),(3,'a'),(3,'b')]

[(x, y) | x <- [1, 2, 3], y <- ['a', 'b']]

In fact, list comprehensions with multiple generators are exactly the list Applicative (and Monad) at work under the hood.

pure for Lists and Maybe

ghci> pure 42 :: Maybe Int
Just 42

ghci> pure 42 :: [Int]
[42]

pure takes a “plain” value and embeds it in the minimal context:

  • For Maybe: a single Just
  • For lists: a single-element list

Functor vs Applicative

We can always express fmap using pure and (<*>):

fmap f x  =  pure f <*> x
          =  f <$> x

This is not a coincidence: the Applicative laws require it.

Applicative strictly generalizes Functor:

  • Functor: apply a plain function to a value in context
  • Applicative: apply a function in context to a value in context; chain multiple arguments

The Applicative Laws

A lawful Applicative instance must satisfy four laws:

Identity:

pure id <*> x  =  x

Homomorphism:

pure f <*> pure x  =  pure (f x)

Applying a pure function to a pure value is the same as lifting the result.

The Applicative Laws (cont.)

Interchange:

u <*> pure y  =  pure ($ y) <*> u

Composition:

pure (.) <*> u <*> v <*> w  =  u <*> (v <*> w)

The laws ensure that pure and (<*>) interact sensibly and that applicative “effects” compose correctly.

What Does “Effect” Mean?

Each Applicative carries a notion of an effect:

Maybe a   -- effect: possible failure
[a]       -- effect: nondeterminism / multiple results

(<*>) runs the effects of both sides and combines them:

  • Maybe: both must succeed; one failure short-circuits
  • Lists: effects multiply – all combinations are explored

What Does “Effect” Mean?

Maybe a   -- effect: possible failure
[a]       -- effect: nondeterminism / multiple results
-- Failure is infectious in Maybe
ghci> (*) <$> Nothing <*> Just 4
Nothing

-- Multiplicity compounds in []
ghci> (*) <$> [1,2] <*> [10,100]
[10,100,20,200]

Summary: fmap vs pure/<*>

Situation Tool
One-argument function, value in context fmap f mx or f <$> mx
Two-argument function, both in context f <$> mx <*> my
Three-argument function f <$> mx <*> my <*> mz
Lift a plain value into context pure x

The pattern f <$> x1 <> x2 <> … <> xn* generalizes normal function application to values living inside a context.

Conclusion

  • Functor maps a plain function over a single value in context
  • Applicative extends this to functions that are themselves in context, enabling multi-argument lifting
  • pure embeds a plain value into the minimal context
  • (<*>) applies a contextual function to a contextual value
  • The idiom f <$> x <> y <> z reads like f x y z but lifts the computation into the applicative context
  • Maybe models failure: one Nothing poisons the whole result
  • [] models nondeterminism: results are all combinations
  • Every Applicative is a Functor; fmap f x = pure f <*> x