Learn You a Haskell for Great Good! - Miran Lipovaca [106]
ghci> [ x*y | x <- [2,5,10], y <- [8,10,11]]
[16,20,22,40,50,55,80,100,110]
We’re just drawing from two lists and applying a function between every combination of elements. This can be done in the applicative style as well:
ghci> (*) <$> [2,5,10] <*> [8,10,11]
[16,20,22,40,50,55,80,100,110]
This seems clearer to me, because it’s easier to see that we’re just calling * between two nondeterministic computations. If we wanted all possible products of those two lists that are more than 50, we would use the following:
ghci> filter (>50) $ (*) <$> [2,5,10] <*> [8,10,11]
[55,80,100,110]
It’s easy to see how pure f <*> xs equals fmap f xs with lists. pure f is just [f], and [f] <*> xs will apply every function in the left list to every value in the right one, but there’s just one function in the left list, so it’s like mapping.
IO Is An Applicative Functor, Too
Another instance of Applicative that we’ve already encountered is IO. This is how the instance is implemented:
instance Applicative IO where
pure = return
a <*> b = do
f <- a
x <- b
return (f x)
Since pure is all about putting a value in a minimal context that still holds the value as the result, it makes sense that pure is just return. return makes an I/O action that doesn’t do anything. It just yields some value as its result, without performing any I/O operations like printing to the terminal or reading from a file.
If <*> were specialized for IO, it would have a type of (<*>) :: IO (a -> b) -> IO a -> IO b. In the case of IO, it takes the I/O action a, which yields a function, performs the function, and binds that function to f. Then it performs b and binds its result to x. Finally, it applies the function f to x and yields that as the result. We used do syntax to implement it here. (Remember that do syntax is about taking several I/O actions and gluing them into one.)
With Maybe and [], we could think of <*> as simply extracting a function from its left parameter and then applying it over the right one. With IO, extracting is still in the game, but now we also have a notion of sequencing, because we’re taking two I/O actions and gluing them into one. We need to extract the function from the first I/O action, but to extract a result from an I/O action, it must be performed. Consider this:
myAction :: IO String
myAction = do
a <- getLine
b <- getLine
return $ a ++ b
This is an I/O action that will prompt the user for two lines and yield as its result those two lines concatenated. We achieved it by gluing together two getLine I/O actions and a return, because we wanted our new glued I/O action to hold the result of a ++ b. Another way of writing this is to use the applicative style:
myAction :: IO String
myAction = (++) <$> getLine <*> getLine
This is the same thing we did earlier when we were making an I/O action that applied a function between the results of two other I/O actions. Remember that getLine is an I/O action with the type getLine :: IO String. When we use <*> between two applicative values, the result is an applicative value, so this all makes sense.
If we return to the box analogy, we can imagine getLine as a box that will go out into the real world and fetch us a string. Calling (++) <$> getLine <*> getLine makes a new, bigger box that sends those two boxes out to fetch lines from the terminal and then presents the concatenation of those two lines as its result.
The type of the expression (++) <$> getLine <*> getLine is IO String. This means that the expression is a completely normal I/O action like any other, which also yields a result value, just like other I/O actions. That’s why we can do stuff like this:
main = do
a <- (++) <$> getLine <*> getLine
putStrLn $ "The two lines concatenated turn out to be: " ++ a
Functions As Applicatives
Another instance of Applicative is (->) r, or functions. We don’t often use functions as applicatives, but the concept is still really interesting, so let’s take a look at how the function instance