Learn You a Haskell for Great Good! - Miran Lipovaca [137]
Chunk "chi" (Chunk "huahua" Empty)
Cool! Now our applyLog can work for any monoid. We need to change the type to reflect this, as well as the implementation, because we need to change ++ to mappend:
applyLog :: (Monoid m) => (a, m) -> (a -> (b, m)) -> (b, m)
applyLog (x, log) f = let (y, newLog) = f x in (y, log `mappend` newLog)
Because the accompanying value can now be any monoid value, we no longer need to think of the tuple as a value and a log; now we can think of it as a value with an accompanying monoid value. For instance, we can have a tuple that has an item name and an item price as the monoid value. We just use the Sum newtype to make sure that the prices are added as we operate with the items. Here’s a function that adds drink to some cowboy food order:
import Data.Monoid
type Food = String
type Price = Sum Int
addDrink :: Food -> (Food, Price)
addDrink "beans" = ("milk", Sum 25)
addDrink "jerky" = ("whiskey", Sum 99)
addDrink _ = ("beer", Sum 30)
We use strings to represent foods and an Int in a Sum newtype wrapper to keep track of how many cents something costs. As a reminder, doing mappend with Sum results in the wrapped values being added together:
ghci> Sum 3 `mappend` Sum 9
Sum {getSum = 12}
The addDrink function is pretty simple. If we’re eating beans, it returns "milk" along with Sum 25, so 25 cents wrapped in Sum. If we’re eating jerky, we drink whiskey. And if we’re eating anything else, we drink beer. Just normally applying this function to a food wouldn’t be terribly interesting right now. But using applyLog to feed a food that comes with a price itself into this function is worth a look:
ghci> ("beans", Sum 10) `applyLog` addDrink
("milk",Sum {getSum = 35})
ghci> ("jerky", Sum 25) `applyLog` addDrink
("whiskey",Sum {getSum = 124})
ghci> ("dogmeat", Sum 5) `applyLog` addDrink
("beer",Sum {getSum = 35})
Milk costs 25 cents, but if we have it with beans that cost 25 cents, we’ll end up paying 35 cents.
Now it’s clear how the attached value doesn’t always need to be a log. It can be any monoid value, and how two such values are combined depends on the monoid. When we were doing logs, they were appended, but now, the numbers are being added up.
Because the value that addDrink returns is a tuple of type (Food, Price), we can feed that result to addDrink again, so that it tells us what we should drink along with our meal and how much that will cost us. Let’s give it a shot:
ghci> ("dogmeat", Sum 5) `applyLog` addDrink `applyLog` addDrink
("beer",Sum {getSum = 65})
Adding a drink to some dog meat results in a beer and an additional 30 cents, so ("beer", Sum 35). And if we use applyLog to feed that to addDrink, we get another beer, and the result is ("beer", Sum 65).
The Writer Type
Now that you’ve seen how a value with an attached monoid acts like a monadic value, let’s examine the Monad instance for types of such values. The Control.Monad.Writer module exports the Writer w a type along with its Monad instance and some useful functions for dealing with values of this type.
To attach a monoid to a value, we just need to put them together in a tuple. The Writer w a type is just a newtype wrapper for this. Its definition is very simple:
newtype Writer w a = Writer { runWriter :: (a, w) }
It’s wrapped in a newtype so that it can be made an instance of Monad and so that its type is separate from a normal tuple. The a type parameter represents the type of the value, and the w type parameter represents the type of the attached monoid value.
The Control.Monad.Writer module reserves the right to change the way it internally implements the Writer w a type, so it doesn’t export the Writer value constructor. However, it does export the writer function, which does the same thing that the Writer constructor would do. Use it when you want to take a tuple and make a Writer value from it.
Because the Writer value constructor is not exported, you also can’t pattern match against it. Instead, you need to use the runWriter function, which takes a tuple that’s wrapped in a Writer newtype