Joseph Haugh
University of New Mexico
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?
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?
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.
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.
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:
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.
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
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
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.
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
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.
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.
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.
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
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
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']]
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.
ghci> pure 42 :: Maybe Int
Just 42
ghci> pure 42 :: [Int]
[42]
pure takes a “plain” value and embeds it in the minimal context:
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:
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.
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.
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 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]
| 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.