FP newbie learns a little about applicatives

A few posts back I learned that functors are types that can be mapped over. The idea is that if we have a function a -> b, we would like to be able to apply that to as in different contexts, such as a list of a, a Maybe a, or an IO action that results in an a. In the previous post we referred to these contexts as "boxes", so we could lift a function a -> b to work on a box of a and return a box of b.

-- "Functor f =>"  just means that `f` refers to a functor type (or a type of box)
fmap :: Functor f => (a -> b) -> f a -> f b 

ghci> fmap succ (Just 4)
Just 5
ghci> fmap (^2) (Just 4)
Just 16
ghci> fmap (++"!") getLine
Hello World
"Hello World!"

All these calls map single argument functions over functors, which is neat, but a bit limiting. What happens if we map a two (or more) argument function like +?

ghci> :t fmap (+) (Just 4)
fmap (+) (Just 4) :: Num a => Maybe (a -> a)

This gives us a +4 function in a Maybe context, so fmap (+) (Just 4) = Just (4+). But how do we pass the second argument to this boxed up function? We can’t use fmap again, because it’s signature takes an (a -> b), not a f (a -> b). But if f is not just a functor, but an applicative functor, then we have another option. Applicative functors still support fmap, but also add some other functions, the main one being:

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

The bizarre-looking <*> function takes a boxed-up function a -> b and applies it to a boxed-up a, which is just what we need in this case.

ghci> import Control.Applicative  -- imports (<*>) function (et al.)
ghci> fmap (+) (Just 4) <*> (Just 10)
Just 14

Applying applicatives

Just as fmap lets us map a function over a value in a context, <*> applies a function in a context to a value in a context. It ends up mirroring standard function application, only within a context:

ghci> let sum3 a b c = a+b+c
ghci> sum3 5 10 15
30
ghci> fmap sum3 (Just 5) <*> (Just 10) <*> (Just 15)
Just 30

The initial fmap call lifts sum3 into the new context (puts it in a box, in this case the Maybe type), then we apply that boxed function to its remaining arguments while staying in that context.

Because this is a fairly common pattern, the Control.Applicative library defines the function <$> as an alias for fmap:

ghci> sum3 <$> (Just 5) <*> (Just 10) <*> (Just 15)
Just 30

Using this syntax we can apply functions with any number of arguments within a context. We apply each argument using <*>.

Lifting functions

There are also some convenience functions, liftA, liftA2 and liftA3 to convert 1, 2 and 3 argument functions that take applicative functor values. From the Haskell source:

-- For unary functions:
liftA :: Applicative f => (a -> b) -> f a -> f b
liftA f a = pure f <*> a

-- For binary functions:
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
liftA2 f a b = f <$> a <*> b

-- For ternary functions:
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d
liftA3 f a b c = f <$> a <*> b <*> c

This lets us use standard function syntax for calling our functions within a context.

ghci> liftA3 sum3 (Just 5) (Just 10) (Just 15)
Just 30

Well-behaved applicatives

Just as there are laws a functor must satisfy, applicative functors have some additional requirements.

First up, as they are defined in Haskell, applicatives have to not only supply a <*> function, but also a pure function, which will take a value and put it in the context of the applicative functor:

pure :: Applicative f => a -> f a

This can be used to lift a function into a context so we can apply it to applicatives. As above, liftA is defined as liftA f a = pure f <*> a, which is equivalent to writing fmap f a. This gives our first applicative property. There’s a good description of all the applicative laws in the Haskell Wikibook, but as a quick summary:

  • fmap law: pure f <*> x = fmap f x
  • Identity: pure id <*> v = v
  • Composition: pure (.) <*> u <*> v *<> w = u <*> (v <*> w)
  • Interchange: u <*> pure y = pure ($ y) <*> u
  • Homomorphism: pure f <*> pure x = pure (f x)

Summary

Applicative functors are functors that support some additional behaviours. While the motivation for functors is to be able to apply a function within a context, the motivation for applicatives is to apply functions with multiple arguments within a context.

We can apply each argument using the <*> operator, giving us expressions like f <$> arg0 <*> arg1 <*> ... <*> argN. We can also lift functions of 1 to 3 arguments using liftA, liftA2 or liftA3.

We saw previously that functors can be quite useful to reuse functions in different contexts, such as when performing IO operations that result in particular values. Applicative functors extends the same benefits to functions that take multiple arguments.

Comments