Applicative functors, Part II
CIS 194 Week 11
1 April 2012
Suggested reading:
- Applicative Functors from Learn You a Haskell
- The Typeclassopedia
We begin with a review of the Functor
and Applicative
type classes:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
Every Applicative
is also a Functor
—so can we implement fmap
in terms of pure
and (<*>)
? Let’s try!
fmap g x = pure g <*> x
Well, that has the right type at least! However, it’s not hard to imagine making Functor
and Applicative
instances for some type such that this equality does not hold. Since this would be a fairly dubious situation, we stipulate as a law that this equality must hold—this is a formal way of stating that the Functor
and Applicative
instances for a given type must “play nicely together”.
Now, let’s see a few more examples of Applicative
instances.
More Applicative Examples
Lists
How about an instance of Applicative
for lists? There are actually two possible instances: one that matches up the list of functions and list of arguments elementwise (that is, it “zips” them together), and one that combines functions and arguments in all possible ways.
First, let’s write the instance that does all possible combinations. (For reasons that will become clear next week, this is the default instance.) From this point of view, lists represent nondeterminism: that is, a value of type [a]
can be thought of as a single value with multiple possibilities. Then (<*>)
corresponds to nondeterministic function application—that is, the application of a nondeterministic function to a nondeterministic argument.
instance Applicative [] where
pure a = [a] -- a "deterministic" value
[] <*> _ = []
(f:fs) <*> as = (map f as) ++ (fs <*> as)
Here’s an example:
names = ["Joe", "Sara", "Mae"]
phones = ["555-5555", "123-456-7890", "555-4321"]
employees1 = Employee <$> names <*> phones
Maybe this particular example doesn’t make that much sense, but it’s not hard to imagine situations where you want to combine things in all possible ways like this. For example, we can do nondeterministic arithmetic like so:
(.+) = liftA2 (+) -- addition lifted to some Applicative context
(.*) = liftA2 (*) -- same for multiplication
-- nondeterministic arithmetic
n = ([4,5] .* pure 2) .+ [6,1] -- (either 4 or 5) times 2, plus either 6 or 1
-- and some possibly-failing arithmetic too, just for fun
m1 = (Just 3 .+ Just 5) .* Just 8
m2 = (Just 3 .+ Nothing) .* Just 8
Next, let’s write the instance that does elementwise combining. First, we must answer an important question: how should we handle lists of different lengths? Some thought reveals that the most sensible thing to do is to truncate the longer list to the length of the shorter, throwing away the extra elements. Of course there are other possible answers: we might, for instance, extend the shorter list by copying the last element (but then what do we do when one of the lists is empty?); or extend the shorter list with a “neutral” element (but then we would have to require an instance of Monoid
, or an extra “default” argument for the application).
This decision in turn dictates how we must implement pure
, since we must obey the law
pure f <*> xs === f <$> xs
Notice that the right-hand side is a list with the same length as xs
, formed by applying f
to every element in xs
. The only way we can make the left-hand side turn out the same… is for pure
to create an infinite list of copies of f
, because we don’t know in advance how long xs
is going to be.
We implement the instance using a newtype
wrapper to distinguish it from the other list instance. The standard Prelude function zipWith
also comes in handy.
newtype ZipList a = ZipList { getZipList :: [a] }
deriving (Eq, Show, Functor)
instance Applicative ZipList where
pure = ZipList . repeat
ZipList fs <*> ZipList xs = ZipList (zipWith ($) fs xs)
An example:
employees2 = getZipList $ Employee <$> ZipList names <*> ZipList phones
Reader/environment
Let’s do one final example instance, for (->) e
. This is known as the reader or environment applicative, since it allows “reading” from the “environment” e
. Implementing the instance is not too hard, we just have to use our nose and follow the types:
instance Functor ((->) e) where
fmap = (.)
instance Applicative ((->) e) where
pure = const
f <*> x = \e -> (f e) (x e)
An Employee
example:
data BigRecord = BR { getName :: Name
, getSSN :: String
, getSalary :: Integer
, getPhone :: String
, getLicensePlate :: String
, getNumSickDays :: Int
}
r = BR "Brent" "XXX-XX-XXX4" 600000000 "555-1234" "JGX-55T3" 2
getEmp :: BigRecord -> Employee
getEmp = Employee <$> getName <*> getPhone
ex01 = getEmp r
Aside: Levels of Abstraction
Functor
is a nifty tool but relatively straightforward. At first glance it seems like Applicative
doesn’t add that much beyond what Functor
already provides, but it turns out that it’s a small addition with a huge impact. Applicative
(and as we will see next week, Monad
) deserves to be called a “model of computation”, while Functor
doesn’t.
When working with things like Applicative
and Monad
, it’s very important to keep in mind that there are multiple levels of abstraction involved. Roughly speaking, an abstraction is something which hides details of a lower level, providing a “high-level” interface that can be used (ideally) without thinking about the lower level—although the details of the lower level often “leak through” in certain cases. This idea of layers of abstraction is widespread. Think about user programs—OS—kernel—integrated circuits—gates—silicon, or HTTP—TCP—IP—Ethernet, or programming languages—bytecode—assembly—machine code. As we have seen, Haskell gives us many nice tools for constructing multiple layers of abstraction within Haskell programs themselves, that is, we get to dynamically extend the “programming language” layer stack upwards. This is a powerful facility but can lead to confusion. One must learn to explicitly be able to think on multiple levels, and to switch between levels.
With respect to Applicative
and Monad
in particular, there are just two levels to be concerned with. The first is the level of implementing various Applicative
and Monad
instances, i.e. the “raw Haskell” level. You gained some experience with this level in your previous homework, when you implemented an Applicative
instance for Parser
.
Once we have an Applicative
instance for a type like Parser
, the point is that we get to “move up a layer” and program with Parser
s using the Applicative
interface, without thinking about the details of how Parser
and its Applicative
instance are actually implemented. You got a little bit of experience with this on last week’s homework, and will get a lot more of it this week. Programming at this level has a very different feel than actually implementing the instances. Let’s see some examples.
The Applicative API
One of the benefits of having a unified interface like Applicative
is that we can write generic tools and control structures that work with any type which is an instance of Applicative
. As a first example, let’s try writing
pair :: Applicative f => f a -> f b -> f (a,b)
pair
takes two values and pairs them, but all in the context of some Applicative f
. As a first try we can take a function for pairing and “lift” it over the arguments using (<$>)
and (<*>)
:
pair fa fb = (\x y -> (x,y)) <$> fa <*> fb
This works, though we can simplify it a bit. First, note that Haskell allows the special syntax (,)
to represent the pair constructor, so we can write
pair fa fb = (,) <$> fa <*> fb
But actually, we’ve seen this pattern before—this is the liftA2
pattern which got us started down this whole Applicative
road. So we can further simplify to
pair fa fb = liftA2 (,) fa fb
but now there is no need to explicitly write out the function arguments, so we reach our final simplified version:
pair = liftA2 (,)
Now, what does this function do? It depends, of course, on the particular f
chosen. Let’s consider a number of particular examples:
f = Maybe
: the result isNothing
if either of the arguments is; if both areJust
the result isJust
their pairing.f = []
:pair
computes the Cartesian product of two lists.f = ZipList
:pair
is the same as the standardzip
function.f = IO
:pair
runs twoIO
actions in sequence, returning a pair of their results.f = Parser
:pair
runs two parsers in sequence (the parsers consume consecutive sections of the input), returning their results as a pair. If either parser fails, the whole thing fails.
Can you implement the following functions? Consider what each function does when f
is replaced with each of the above types.
(*>) :: Applicative f => f a -> f b -> f b
mapA :: Applicative f => (a -> f b) -> ([a] -> f [b])
sequenceA :: Applicative f => [f a] -> f [a]
replicateA :: Applicative f => Int -> f a -> f [a]
Generated 2013-04-04 15:15:39.334627