Learn You a Haskell for Great Good! - Miran Lipovaca [105]
So far, we’ve used only Maybe in our examples, and you might be thinking that applicative functors are all about Maybe. There are loads of other instances of Applicative, so let’s meet them!
Lists
Lists (actually the list type constructor, []) are applicative functors. What a surprise! Here’s how [] is an instance of Applicative:
instance Applicative [] where
pure x = [x]
fs <*> xs = [f x | f <- fs, x <- xs]
Remember that pure takes a value and puts it in a default context. In other words, it puts it in a minimal context that still yields that value. The minimal context for lists would be the empty list, but the empty list represents the lack of a value, so it can’t hold in itself the value on which we used pure. That’s why pure takes a value and puts it in a singleton list. Similarly, the minimal context for the Maybe applicative functor would be a Nothing, but it represents the lack of a value instead of a value, so pure is implemented as Just in the instance implementation for Maybe.
Here’s pure in action:
ghci> pure "Hey" :: [String]
["Hey"]
ghci> pure "Hey" :: Maybe String
Just "Hey"
What about <*>? If the <*> function’s type were limited to only lists, we would get (<*>) :: [a -> b] -> [a] -> [b]. It’s implemented with a list comprehension. <*> must somehow extract the function out of its left parameter and then map it over the right parameter. But the left list can have zero functions, one function, or several functions inside it, and the right list can also hold several values. That’s why we use a list comprehension to draw from both lists. We apply every possible function from the left list to every possible value from the right list. The resulting list has every possible combination of applying a function from the left list to a value in the right one.
We can use <*> with lists like this:
ghci> [(*0),(+100),(^2)] <*> [1,2,3]
[0,0,0,101,102,103,1,4,9]
The left list has three functions, and the right list has three values, so the resulting list will have nine elements. Every function in the left list is applied to every function in the right one. If we have a list of functions that take two parameters, we can apply those functions between two lists.
In the following example, we apply two function between two lists:
ghci> [(+),(*)] <*> [1,2] <*> [3,4]
[4,5,5,6,3,4,6,8]
<*> is left-associative, so [(+),(*)] <*> [1,2] happens first, resulting in a list that’s the same as [(1+),(2+),(1*),(2*)], because every function on the left gets applied to every value on the right. Then [(1+),(2+),(1*),(2*)] <*> [3,4] happens, which produces the final result.
Using the applicative style with lists is fun!
ghci> (++) <$> ["ha","heh","hmm"] <*> ["?","!","."]
["ha?","ha!","ha.","heh?","heh!","heh.","hmm?","hmm!","hmm."]
Again, we used a normal function that takes two strings between two lists of strings just by inserting the appropriate applicative operators.
You can view lists as nondeterministic computations. A value like 100 or "what" can be viewed as a deterministic computation that has only one result, whereas a list like [1,2,3] can be viewed as a computation that can’t decide on which result it wants to have, so it presents us with all of the possible results. So when you write something like (+) <$> [1,2,3] <*> [4,5,6], you can think of it as adding together two nondeterministic computations with +, only to produce another nondeterministic computation that’s even less sure about its result.
Using the applicative style on lists is often a good replacement for list comprehensions. In Chapter 1, we wanted