After reading lesson 29, you’ll be able to
In the preceding lesson, you learned how to use the <*> (pronounced app) operator to extend the power of Functor’s <$> (pronounced fmap) operator. In this lesson, you’ll take a closer look at the Applicative type class. You’ll explore the difference between types that represent a container and types that represent a context. You’ll finish by looking at the powerful things you can achieve by using lists as a context.
A breakfast place offers you the choice of the following:
What are all the possible meals you could choose, and how can you use List to help?
The Applicative type class allows you to use functions that are inside a context, such as Maybe or IO. As you saw in the preceding lesson, this extends the power of the Functor type class. Because of the way that Applicative works with Functor, Functor is a superclass of Applicative. See figure 29.1.
One tricky thing in this definition is that there are two constraints on your type variable f. The first says that f is a Functor, which enforces that Applicative must be a Functor, and then you define f as your stand-in for Applicative. In your method signatures, f is a variable for any Applicative (see figure 29.2). Notice that the operator <*> has the same type signature as your fmap, except the function argument is also in a context. This small difference in <*> allows you to chain together larger sequences of functions inside members of the Functor type class. Here are a few examples of using <$> and <*> to perform mathematical operations on Maybe types:
GHCi> (*) <$> Just 6 <*> Just 7 Just 42 GHCi> div <$> Just 6 <*> Just 7 Just 0 GHCi> mod <$> Just 6 <*> Just 7 Just 6
This solution may seem either elegant or confusing, depending on how comfortable you are with Haskell’s infix binary operators. Undoubtedly, using <$> and <*> can initially seem confusing. Further complicating the issue is that unlike <$> and fmap, <*> has no equivalent function. If you’re struggling with these operators, the best solution is practice. Try going back to some of the examples in unit 1 and change values used as arguments to Maybe values. Remember, because of fmap and <*>, you don’t need to rewrite any functions to work with Maybe values.
Use <$> and <*> to combine two Maybe String types with ++.
The function pure is the second method required by the Applicative type class. The pure method is a useful helper function for taking an ordinary value or function and putting it into a context. The best way to understand pure is to play around with it in GHCi. In the example of a Maybe, pure will return a Just:
GHCi> pure 6 :: Maybe Int Just 6
You can also use pure to put a function into the context of Applicative. For example, if you want to add 6 to (Just 5), you can use either fmap or pure:
GHCi> (6+) <$> Just 5 Just 11 GHCi> pure (6+) <*> Just 5 Just 11
Though these examples are fairly simple, in practice you’ll frequently want a quick way to transform a value into the desired Applicative type. In our visual language, pure also performs an important role, as shown in figure 29.3.
Because of pure, you can take any value that’s not in a context and trivially put it in one. This is vital to allowing all possible computations in a context.
Make the String "Hello World" into an IO String.
So far, we’ve talked somewhat loosely about the difference between parameterized types that represent containers and contexts. At this point, you need to be a bit clearer about what you mean by these terms. The reason is that unlike Functor, Applicative and the next lesson’s type class Monad make sense only when you’re talking about types as a context. Here’s the distinction in a nutshell:
Let’s explore this further. The best test of whether a type is a container is whether you can tell what it does, independent of its name. For example, consider the 2-tuple (a,b). You could implement the same type named Blah.
data Blah a b = Blah a b
Even with a completely useless name like Blah, it’s hard to argue that this type is any different than a regular tuple pair (a,b).
The Data.Map type is another, similar structure. You could call this a Dictionary, a Binary-SearchTree, a MagicLookupBox, and so forth. But what the type means is implied by the data structure itself. Even if the entire set of functions around Map were written in an alien language, you’d still quickly figure out what the type is used for. (Probably...eventually.)
It’s worth pointing out that the 2-tuple (,) and Data.Map are both instances of Functor but not instances of Applicative. Remember that the key power of Applicative is that it lets you apply a function in a parameterized type. Another good way to differentiate containers from contexts is to ask, “Does it make sense to have a <type> function?” Maybe (a -> b) is a function from a -> b that itself might not exist because it was made via partial application with a Nothing value. An IO (a -> b) function is any function that’s operating in the world of IO. What does a Data.Map function mean? Likewise, what does a 2-tuple function mean? If you can answer these questions, you have the starting ground for figuring out the Applicative instance for these types.
When a type is a context, on the other hand, extra information is implied about the type, beyond its structure. The most obvious case is the IO type. When you first introduce parameterized types, you introduce the idea of a Box type.
data Box a = Box a
Clearly, Box is a uselessly trivial type. But there’s no difference at the data constructor level between Box and IO. IO, from our perspective, is just a data constructor that wraps types that came from IO (there’s much more to the IO type, but not that you can immediately see).
The Maybe type is another context type. Suppose you want to create a parameterized type for a resource-constrained computation. You could imagine this type as follows.
data ResourceConstrained a = NoResources | Okay a
Behind the scenes, there would be a lot of magic determining resource usage, but this type is no different from Maybe at the type-constructor level. Most of the information about this type is in a context that you assume about the type itself.
The best way to understand this distinction between container and context is to look at an example. List turns out to be the perfect case. It’s easy to understand List as a container because it’s a common data structure. But List also describes a context. If you understand how List can be a container and a context, you’re on the path to truly understanding Applicatives.
Suppose you want to make it so that (pure +) <*> (1,2) <*> (3,4) = (1+2,1+4,2+3,2+4) = (3,5,5,6). Why doesn’t this work?
This doesn’t work because (3,5,5,6) is an entirely different type than (1,2) or (3,4). The first is type (a,b,c,d), and the other two are (a,b).
The List type, being a fundamental example of nearly everything in Haskell, is both a container and a context. List as a container is easy to understand. List is basically a chain of buckets of whatever type of data you want to hold. But List is a member of Applicative, so there must be a way to view List as a context.
The reason context matters for a list is that to use Applicative, you need to be able to answer the question, “What does it mean to apply a function to two or more values in the context of a list?” For example, what does [1000,2000,3000] + [500,20000] mean? The naive assumption might be as follows:
[1000,2000,3000] + [500,20000] = [1000,2000,3000,500,20000]
But this would be just adding two lists, which is concatenation (the ++ operator for lists). What you’re curious about is what it means to combine two values in the context of List by using addition. In terms of Applicative, you’d read this statement as follows:
pure (+) <*> [1000,2000,3000] <*> [500,20000]
The List data structure alone doesn’t give you enough information to say what this means. To understand how you add two values in a list, you need extra context to interpret the general idea of applying a binary function to values in lists.
The best way to understand List as a context is that it describes nondeterministic computation. Normally, you think of programming as purely deterministic. Each step in the computation is followed by another in a precise order that yields one, final result. In nondeterministic computing, you’re computing multiple possibilities all at once. In terms of thinking nondeterministically, when you add values in the context of a list, you’re adding together all possible values from the two contexts. You can see the somewhat surprising result of using <*> with Lists in GHCi:
GHCi> pure (+) <*> [1000,2000,3000] <*> [500,20000] [1500,21000,2500,22000,3500,23000]
Adding together two Ints in the context of a List means adding all possible combinations of the values in those lists.
It’s important to take a moment and point out the major differences between a list as a container and a list as a context:
Don’t be fooled by your familiarity with List as a container. Both Maybe and IO are much simpler contexts to reason about. A Maybe Int is a single Int value in the context that it may be missing. An IO Int is an Int value in the context that it was produced by an IO action that may have produced side effects or other issues. An [Int] is an Int in the context that there are many possible values for that Int. Because there are many possible values for that [Int], when you apply a function (Int -> Int -> Int) in the context of a list, you must think nondeterministically and compute all possible results of that operation.
As an example, suppose you’re on a game show and you get to choose one of three doors and then one of two boxes. Behind the doors are prizes worth $1,000, $2,000, and $3,000. You don’t know which you’ll get, so you can represent this as a list.
doorPrize :: [Int] doorPrize = [1000,2000,3000]
After the door, you choose one of two boxes; a box can contain either $500 or $20,000. You can represent these possibilities as a list as well.
boxPrize :: [Int] boxPrize = [500,20000]
In a deterministic context, you can open only one door, and pick only one box, getting only one prize. But if you nondeterministically think about this problem, you’re computing all possible combinations of doors you can open and boxes you can pick. Deterministically, if you want to talk about prizes, you’re talking about the one prize you can win. Figure 29.4 presents the deterministic and nondeterministic ways to understand your prize.
The equation for your totalPrize in a deterministic world would be the following (you use the prefix (+) so it’s easy to compare with the Applicative version).
totalPrize :: Int totalPrize = (+) doorPrize boxPrize
In a nondeterministic context, you’re talking about all possible prizes that can be won. You can describe the nondeterministic totalPrize with the function shown in figure 29.5.
In GHCi, you can see that totalPrize represents all possible prizes that can be won:
GHCi> totalPrize [1500,21000,2500,22000,3500,23000]
When you add two lists in context, you get the combination of all possible worlds. For each door prize, you can pick one of the two possible box prizes. The results of adding two lists within the context of a list are all possible solutions in your nondeterministic world.
Next you’ll look at two more examples of practical nondeterministic computing. You’ll use nondeterministic computing to compute all nonprime numbers, allowing you to easily identify primes. Then you’ll use nondeterministic computing to quickly generate a set of possible test data.
Solve this problem if the boxes contain a prize multiplier instead of just an additional prize. The multipliers are 10× and 50×.
boxMultiplier = [10,50] newOutcomes = pure (*) <*> doorPrize <*> boxMultiplier GHCi> newOutcomes [10000,50000,20000,100000,30000,150000]
A prime number is any number divisible by only 1 and itself. Suppose you want to generate a list of prime numbers. There’s an amazingly simple method for computing a list of primes using the Applicative properties of a list. Here’s the basic process:
The only question remaining then is, “How do you compute the composite numbers?” A composite number is any number that results from multiplying two or more other numbers together. You can easily build this list by multiplying each number in your [2 .. n] list by itself. How can you do this easily? With Applicative! For example, if you have [2..4], you can use multiplication *, pure, and <*> to build your list of all possible numbers that are made from these numbers:
GHCi> pure (*) <*> [2 .. 4] <*> [2 .. 4] [4,6,8,6,9,12,8,12,16]
This list is inefficient, as it includes numbers out of your range as well as duplicate numbers. But it’ll include every composite number in your range. Given this bit of code, you can easily write your function for listing all prime numbers to n.
primesToN :: Integer -> [Integer] primesToN n = filter isNotComposite twoThroughN where twoThroughN = [2 .. n] composite = pure (*) <*> twoThroughN <*> twoThroughN isNotComposite = not . (`elem` composite)
Although not the most efficient prime-number-generating algorithm, this is incredibly easy to implement and works well enough for reasonably small ranges:
GHCi> primesToN 32 [2,3,5,7,11,13,17,19,23,29,31]
If you ever need to whip up a quick prime-number generator, this little trick can be a useful tool to have.
In the preceding lesson, we showed how a User could be created in different contexts by using Applicative. You used this User type for a user in a video game:
data User = User { name :: String , gamerId :: Int , score :: Int } deriving Show
You used Applicative to create instances of User in the context of both IO and Maybe. To demonstrate how powerful the list context is, you’ll do the same thing, only to create a large amount of test data.
Suppose you have a list of usernames, some typical and others problematic in certain cases. Thinking of lists as context, testNames represents possible names.
testNames :: [String] testNames = ["John Smith" ,"Robert'); DROP TABLE Students;--" ,"Christina NULL" ,"Randall Munroe"]
You want to test possible gamer IDs with gamerIds.
testIds :: [Int] testIds = [1337 ,0123 ,999999]
And you also want to make sure you have possible troublesome scores as well.
testScores :: [Int] testScores = [0 ,100000 ,-99999]
What you want to do is generate test data that includes all possible combinations of these values. This means nondeterministically computing a list of possible users. You could do that by hand, but that would mean writing out 4 × 3 × 3 = 36 entries! Plus, if you later decide to add another value to any of those lists, it means a lot of work for you.
Instead, you can use the Applicative properties of List to nondeterministically generate your test data for you. You’ll do this exactly the same way you created User types for IO and Maybe in the preceding lesson.
testData :: [User] testData = pure User <*> testNames <*> testIds <*> testScores
In GHCi, you can see that you’ve successfully created a list of all 36 possible combinations of these values. Even better, to update your list, you have to add whatever values you like to testNames, testIds, or testScores:
GHCi> length testData 36 GHCi> take 3 testData [User {name = "John Smith", gamerId = 1337, score = 0} ,User {name = "John Smith", gamerId = 1337, score = 100000} ,User {name = "John Smith", gamerId = 1337, score = -99999}]
Using the List type to perform nondeterministic computing shows how powerful the Applicative type class can be when working with contexts!
Add your own name to testNames and regenerate the data. How many examples are there now?
testNames = ["Will Kurt" , "John Smith" ,"Robert'); DROP TABLE Students;--" ,"Christina NULL" ,"Randall Munroe"] testData :: [User] testData = pure User <*> testNames <*> testIds <*> testScoresThere are now 45 examples.
In this lesson, our objective was to give you a deeper insight into the Applicative type class. You were formally introduced to the full Applicative type class, which includes the <*> operator you learned in the preceding lesson, as well as the pure method. The role of pure is to take normal values and put them into the context you need; for example, turning an Int into a Maybe Int. You also focused on the differences between containers and contexts by exploring a list as a context. Contexts differ from containers in that they require you to understand something about the computation happening beyond what the data structure alone tells you. For lists, this means representing nondeterministic computation, rather than just a container for sequential data. Let’s see if you got this.
To prove that Applicative is strictly more powerful than Functor, write a universal version of fmap, called allFmap, that defines fmap for all members of the Applicative type class. Because it works for all instances of Applicative, the only functions you can use are the methods required by the Applicative type class. To get you started, here’s your type signature:
allFmap :: Applicative f => (a -> b) -> f a -> f bWhen you’re finished, test this out on List and Maybe, which are both members of Applicative:
GHCi> allFmap (+ 1) [1,2,3] [2,3,4] GHCi> allFmap (+ 1) (Just 5) Just 6 GHCi> allFmap (+ 1) Nothing Nothing
Translate the following expression into one where the result is a Maybe Int. The catch is that you may not add (or remove) anything to the code except pure and <*>. You can’t use the Just constructor or any extra parentheses.
example :: Int example = (*) ((+) 2 4) 6Here’s the type signature for your answer:
exampleMaybe :: Maybe Int
Take the following example and use nondeterministic computing with Lists to determine how much beer you need to purchase to assure there will be enough:
- You bought beer last night but don’t remember whether it was a 6-pack or a 12-pack.
- You and your roommate each had two beers last night.
- You’re having either two or three friends coming over tonight, depending on who can come.
- For a long night of gaming, you expect the average person to drink three to four beers.