Chapter 17: Applicative¶
17.1 Applicative¶
Applicatives are monoidal functors. The applicative type class allows for function application lifted over structure (like functor). But with applicative the function we’re applying is also embedded in some structure.
In this chapter, we will:
define and explore the
Applicative
type class and its core operations;demonstrate why applicatives are monoidal functors;
make the usual chitchat about laws and instances;
do a lot of lifting;
give you some
Validation
.
17.2 Defining Applicative¶
The first thing you’ll notice here is that the type
argument to Applicative
is also a functor.
·∾ :info Applicative
class Functor f => Applicative (f :: * -> *) where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
{-# MINIMAL pure, ((<*>) | liftA2) #-}
instance Applicative (Either e) -- Defined in Data.Either.
instance Applicative []
instance Applicative Maybe
instance Applicative IO
instance Applicative ((->) a)
instance Monoid a =>
Applicative ((,) a)
The two core operations of Applicative
are
pure
and <*>
(pronounced “apply”).
pure
lifts some value into an applicative
context.
The <*>
operator is described as “sequential
application” (or sometimes “apply” or just “ap”).
It applies monoids contained in one functor to
the contents of another functor (as seen above).
Along with these core functions, the
Control.Applicative
module provides some other
convenient functions:: liftA
, liftA2
, and
liftA3
.
liftA :: Applicative f
=> (a -> b) -> f a -> f b
liftA2 :: Applicative f
=> (a -> b -> c) -> f a -> f b -> f b
liftA2 :: Applicative f
=> (a -> b -> c -> d) -> f a -> f b -> f b -> f d
17.3 Functor vs. Applicative¶
Let’s review the difference between fmap
and
<*>
:
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
The difference is we now have an f
in front of
our function (a -> b)
. The increase in power it
introduces is profound.
For one thing, any Applicative
also has a Functor
and not merely by definition – you can define a functor
in terms of a provided Applicative
instance.
fmap f x
\(=\) pure f <*> x
Here’s an example:
·∾ import Control.Applicative
·∾ fmap (+1) [1,2,3]
[2,3,4]
·∾ pure (+1) <*> [1,2,3]
[2,3,4]
Keeping in mind that pure has type Applicative f => a
-> f a
, we can think of it as a means of embedding a
value of any type in the structure we’re working with:
·∾ pure 1 :: [Int]
[1]
·∾ :type it
it :: [Int]
·∾ pure 1 :: Maybe Int
Just 1
·∾ :type it
it :: Maybe Int
·∾ pure 1 :: Either a Int
Right 1
·∾ :type it
it :: Either a Int
·∾ pure 1 :: ([a], Int)
([],1)
·∾ :type it
it :: ([a], Int)
The left type is handled differently from the right in the final two examples for the same reason as here:
·∾ fmap (+1) (4,5)
(4,6)
The left type is part of the structure, and the structure is not transformed by the function application.
17.4 Applicative functors are monoidal functors¶
First let us notice something:
($) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
In the type signature above, mappend
is our
function from (a -> b)
. Each operator lifts
a bit more. <$>
lifts the output into a
functorial context, and <*>
additionally
has its input function within a functor.
·∾ (Just (*2)) <*> (Just 2)
Just 4
·∾ (Just (*2)) <*> Nothing
Nothing
·∾ Nothing <*> (Just 2)
Nothing
·∾ Nothing <*> Nothing
Nothing
With Maybe
, the ordinary functor is mapping
over the possibility of a value’s nonexistence.
With the Applicative
, now the function also
might not be provided.
17.4.1 Show me the monoids¶
Recall that the Functor
instance for the two-tuple
ignores the first value inside the tuple:
·∾ fmap (+1) ("blah",0)
("blah",1)
·∾ :info (,)
data (,) a b = (,) a b -- Defined in ‘GHC.Tuple’
instance Monoid a => Applicative ((,) a) -- Defined in ‘GHC.Base’
instance (Monoid a, Monoid b) => Monoid (a, b) -- Defined in ‘GHC.Base’
Alright, that makes sense so far. But what about this?
·∾ ("Woo", (+1)) <*> (" Hoo!",0)
("Woo Hoo!",1)
The first elements of the pair were combined without explicitly supplying a function. What has happened, here?
No explanation for you. What about this?
·∾ import Data.Monoid
·∾ (Sum 2, (+1)) <*> (Sum 0, 0)
(Sum {getSum = 2},1)
·∾ (Sum 2,(+1)) <*> (Sum 0,0)
(Sum {getSum = 2},1)
·∾ (Product 3,(+9)) <*> (Product 2,8)
(Product {getProduct = 6},17)
·∾ (All True,(+1)) <*> (All False,0)
(All {getAll = False},1)
17.4.2 Tuple Monoid and Applicative side by side¶
Squint if you can’t see it:
instance (Monoid a, Monoid b) => Monoid (a,b) where
mempty = (mempty, mempty)
(a,b) `mappend` (a',b') = (a `mappend` a', b `mappend` b')
instance Monoid a => Applicative ((,) a) where
pure x = (mempty, x)
(u,f) <*> (v,x) = (u `mappend` v, f x)
17.4.3 Maybe Monoid and Applicative¶
While applicatives are monoidal functors, be careful about making assumptions based on this.
For one thing, Monoid
and Applicative
instances
aren’t required or guaranteed to have the same monoid of
structure, and the functorial part may change the way it
behaves.
17.5 Applicative in use¶
This section provides usage examples of several
instances of Applicative
.
17.5.1 List Applicative¶
Let’s start by specializing the types:
-- 17.5.1 List Applicative
-- page 690
-- f ≡ []
(<*>) :: f (a -> b) -> f a -> f b
(<*>) :: [ ] (a -> b) -> [ ] a -> [ ] b
-- more syntactically typical
(<*>) :: [(a -> b)] -> [a] -> [b]
pure :: a -> f a
pure :: a -> [ ] a
-- https://gitlab.haskell.org/ghc/ghc/-/wikis/type-application
-- Or, using GHCi, you can do this:
·∾ :set -XTypeApplications
·∾ :type (<*>) @[]
(<*>) @[] :: [a -> b] -> [a] -> [b]
·∾ :type pure @[]
pure @[] :: a -> [a]
17.5.2 What’s the List applicative do?¶
Previously with list Functor
, we were
mapping a single function over a plurality of
values:
·∾ fmap (2^) [1,2,3]
[2,4,8]
With the list applicative, we are mapping a plurality of functions over a plurality of values.
·∾ [(+1),(*2)] <*> [2,4]
[3,5,4,8]
Now what happened with that expression we tested? Something like this:
[(+1),(*2)] <*> [2,4]
≡[(+1)2,(+1)4,(*2)2,(*2)4]
≡[3,5,4,8]
Apply maps each function value from the first list over the second list, applies the operation, and returns one list.
The liftA2
function gives us another way to
write this, too:
·∾ import Control.Applicative
·∾ liftA2 (+) [1,2] [3,5]
[4,6,5,7]
If you’re familiar with Cartesian products, this probably looks a lot like one, but with functions.
There are a ton of examples in this section that aren’t included inline in my notes here. So, here is a terminal recording of me typing all of them out, to serve as proof (to myself) that I actually did try them. It’s long and messy, so I don’t recommend you watch it.
Instead, you’ll find a more nicely formatted version of those ghci examples in the figures directory.
17.5.3 Exercises: Lookups¶
In the following exercises you will need to use these terms to make the expressions typecheck.
pure :: Applicative f => a -> f a
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<*>) :: Applicative f => f (a -> b) -> f a -> f b
Make the following expressions typecheck:
added :: Maybe Integer added = (+3) (lookup 3 $ zip [1,2,3] [4,5,6])
Alright, here is the first one:
added :: Maybe Integer added = pure (+3) <*> (lookup 3 $ zip [1,2,3] [4,5,6])
y :: Maybe Integer y = lookup 3 $ zip [1,2,3] [4,5,6] z :: Maybe Integer z = lookup 2 $ zip [1,2,3] [4,5,6] tupled :: Maybe (Integer, Integer) tupled = (,) y z
Now it typechecks!
y :: Maybe Integer y = lookup 3 $ zip [1,2,3] [4,5,6] z :: Maybe Integer z = lookup 2 $ zip [1,2,3] [4,5,6] tupled :: Maybe (Integer, Integer) tupled = pure (,) <*> y <*> z
import Data.List (elemIndex) x :: Maybe Int x = elemIndex 3 [1,2,3,4,5] y :: Maybe Int y = elemIndex 4 [1,2,3,4,5] max' :: Int -> Int -> Int max' = max maxed :: Maybe Int maxed = max' x y
This should work:
x' :: Maybe Int x' = elemIndex 3 [1,2,3,4,5] y' :: Maybe Int y' = elemIndex 4 [1,2,3,4,5] max' :: Int -> Int -> Int max' = max maxed :: Maybe Int maxed = pure max' <*> x' <*> y'
as = [1,2,3] bs = [4,5,6] a :: Maybe Integer a = lookup 3 (zip as bs) b :: Maybe Integer b = lookup 2 (zip as bs) summed :: Maybe Integer summed = sum $ (,) a b
This should work:
as = [1,2,3] bs = [4,5,6] a :: Maybe Integer a = lookup 3 (zip as bs) b :: Maybe Integer b = lookup 2 (zip as bs) summed :: Maybe Integer summed = sum <$> (pure (,) <*> a <*> b)
17.5.4 Identity¶
The Identity
type here is a way to introduce
structure without changing the semantics of what
we’re doing.
Specializing the types¶
Here is what the type will look like when our structure is Identity
-- 17.5.4 Identity
-- Specializing the types
-- page 704
-- This type comes from the last chapter on Foldable
data Identity a = Identity a
type Id = Identity
-- f ~ Identity
-- Applicative f =>
(<*>) :: f (a -> b) -> f a -> f b
(<*>) :: Id (a -> b) -> Id a -> Id b
pure :: a -> f a
pure :: a -> Id a
17.6 Applicative laws¶
After examining the law, try each expression in the REPL.
Identity¶
id
embedded in an applicative applied to a value
results in that value:
pure id <*> v = v
Composition¶
The result of composing our functions first and then applying them should be the same as the result of applying our functions first and then composing them.
pure (.) <*> u <*> v <*> w = u <*> (v <*> w)
This law is meant to ensure that there are no surprises resulting from composing our function applications.
Homomorphism¶
A homomorphism is a structure-preserving map between two structures.
pure f <*> pure x = pure (f x)
The general idea is that applying the function doesn’t change the structure around the value.
Interchange¶
Here is a rephrasing of the interchange law without
($)
that I find easier to read:
a <*> pure v = pure (\f -> f v) <*> a
In this phrasing,
a
is an applicative with a function in it, andv
is an applicative with a value in it.
An applicative containing functions applied to an applicative of values in the same context is equivalent to applying that function to every member element of the applicative containing values.
17.7 You knew this was coming¶
This section has you set up a project to property
test the Applicative
type class laws using a
hackage package known as checkers
.
Check out exercises/17.7.1_-_bad_monoid.rst.d/bad-monoid/
for more.
17.9 Chapter Exercises¶
17.9.1 Specialize the types¶
Given a type that has an instance of
Applicative
, specialize the types of
the methods. Test your specialization
in the REPL.
One way to do this is to bind aliases of the typeclass methods to more concrete types that have the type we told you to fill in.
-- Type [] -- Methods pure :: a -> ? a (<*>) :: ? (a -> b) -> ? a -> ? b
This one can be solved with ghci:
·∾ :set -XTypeApplications ·∾ :type pure @[] pure @[] :: a -> [a] ·∾ :type (<*>) @[] (<*>) @[] :: [a -> b] -> [a] -> [b]
-- Type IO -- Methods pure :: a -> ? a (<*>) :: ? (a -> b) -> ? a -> ? b
This one can also be solved directly with ghci:
·∾ :type pure @IO pure @IO :: a -> IO a ·∾ :type (<*>) @IO (<*>) @IO :: IO (a -> b) -> IO a -> IO b
-- Type (,) a -- Methods pure :: a -> ? a (<*>) :: ? (a -> b) -> ? a -> ? b
-- Type (->) e -- Methods pure :: a -> ? a (<*>) :: ? (a -> b) -> ? a -> ? b
17.9.2 Write Applicative instances¶
Write instances for the following datatypes. Confused? Write out what the type should be. Use the checkers library to validate the instances.
data Pair a = Pair a a deriving Show
This should look familiar.
data Two a b = Two a b
data Three a b c = Three a b c
data Three' a b = Three' a b b
data Four a b c d = Four a b c d
data Four' a b = Four' a a a b
17.9.3 Combinations¶
Remember the vowels and stops exercise in the folds
chapter? Write the function to generate the possible
combinations of three input lists using liftA3
from Control.Applicative
.
import Control.Applicative (liftA3)
stops :: String
stops = "pbtdkg"
vowels :: String
vowels = "aeiou"
combos :: [a] -> [b] -> [c] -> [(a, b, c)]
combos = undefined