Learn You a Haskell for Great Good! - Miran Lipovaca [136]
When we were exploring the Maybe monad in the previous chapter, we made a function applyMaybe. This function takes a Maybe a value and a function of type a -> Maybe b. We feed that Maybe a value into the function, even though the function takes a normal a instead of a Maybe a. It does this by minding the context that comes with Maybe a values, which is that they are values with possible failure. But inside the a -> Maybe b function, we can treat that value as just a normal value, because applyMaybe (which later becomes >>=) takes care of checking if it is a Nothing or a Just value.
In the same vein, let’s make a function that takes a value with an attached log—that is, an (a, String) value—and a function of type a -> (b, String), and feeds that value into the function. We’ll call it applyLog. But an (a, String) value doesn’t carry with it a context of possible failure, but rather a context of an additional log value. So, applyLog will make sure that the log of the original value isn’t lost, but is joined together with the log of the value that results from the function. Here’s the implementation of applyLog:
applyLog :: (a, String) -> (a -> (b, String)) -> (b, String)
applyLog (x, log) f = let (y, newLog) = f x in (y, log ++ newLog)
When we have a value with a context that we want to feed to a function, we usually try to separate the actual value from the context, apply the function to the value, and then see whether the context is handled. In the Maybe monad, we checked if the value was a Just x, and if it was, we took that x and applied the function to it. In this case, it’s very easy to find the actual value, because we’re dealing with a pair where one component is the value and the other a log. So, first, we just take the value, which is x, and we apply the function f to it. We get a pair of (y, newLog), where y is the new result and newLog is the new log. But if we returned that as the result, the old log value wouldn’t be included in the result, so we return a pair of (y, log ++ newLog). We use ++ to append the new log to the old one.
Here’s applyLog in action:
ghci> (3, "Smallish gang.") `applyLog` isBigGang
(False,"Smallish gang.Compared gang size to 9.")
ghci> (30, "A freaking platoon.") `applyLog` isBigGang
(True,"A freaking platoon.Compared gang size to 9.")
The results are similar to before, except that now the number of people in the gang has its accompanying log, which is included in the result log.
Here are a few more examples of using applyLog:
ghci> ("Tobin", "Got outlaw name.") `applyLog` (\x -> (length x, "Applied length."))
(5,"Got outlaw name.Applied length.")
ghci> ("Bathcat", "Got outlaw name.") `applyLog` (\x -> (length x, "Applied length."))
(7,"Got outlaw name.Applied length.")
See how inside the lambda, x is just a normal string and not a tuple, and how applyLog takes care of appending the logs?
Monoids to the Rescue
Right now, applyLog takes values of type (a, String), but is there a reason that the log must be a String? It uses ++ to append the logs, so wouldn’t this work on any kind of list, not just a list of characters? Sure, it would. We can change its type to this:
applyLog :: (a, [c]) -> (a -> (b, [c])) -> (b, [c])
Now the log is a list. The type of values contained in the list must be the same for the original list as well as for the list that the function returns. Otherwise, we wouldn’t be able to use ++ to stick them together.
Would this work for bytestrings? There’s no reason it shouldn’t. However, the type we have now works only for lists. It seems as though we would need to make a separate applyLog for bytestrings. But wait! Both lists and bytestrings are monoids. As such, they are both instances of the Monoid type class, which means that they implement the mappend function. And for both lists and bytestrings, mappend is for appending. Watch it in action:
ghci> [1,2,3] `mappend` [4,5,6]
[1,2,3,4,5,6]
ghci> B.pack [99,104,105] `mappend` B.pack [104,117,97,104,117,97]