Learn You a Haskell for Great Good! - Miran Lipovaca [139]
Here’s multWithLog with some extra reporting included:
multWithLog :: Writer [String] Int
multWithLog = do
a <- logNumber 3
b <- logNumber 5
tell ["Gonna multiply these two"]
return (a*b)
It’s important that return (a*b) is the last line, because the result of the last line in a do expression is the result of the whole do expression. Had we put tell as the last line, the result of this do expression would be (). We would lose the result of the multiplication. However, the log would be the same. Here’s this in action:
ghci> runWriter multWithLog
(15,["Got number: 3","Got number: 5","Gonna multiply these two"])
Adding Logging to Programs
Euclid’s algorithm takes two numbers and computes their greatest common divisor—that is, the biggest number that still divides both of them. Haskell already features the gcd function, which does exactly this, but let’s implement our own function and then equip it with logging capabilities. Here’s the normal algorithm:
gcd' :: Int -> Int -> Int gcd' a b
| b == 0 = a
| otherwise = gcd' b (a `mod` b)
The algorithm is very simple. First, it checks if the second number is 0. If it is, then the result is the first number. If it isn’t, then the result is the greatest common divisor of the second number and the remainder of dividing the first number with the second one.
For instance, if we want to know what the greatest common divisor of 8 and 3 is, we just follow this algorithm. Because 3 isn’t 0, we need to find the greatest common divisor of 3 and 2 (if we divide 8 by 3, the remainder is 2). Next, we find the greatest common divisor of 3 and 2. 2 still isn’t 0, so now we have have 2 and 1. The second number isn’t 0, so we run the algorithm again for 1 and 0, as dividing 2 by 1 gives us a remainder of 0. And finally, because the second number is now 0, the final result is 1. Let’s see if our code agrees:
ghci> gcd' 8 3
1
It does. Very good! Now, we want to equip our result with a context, and the context will be a monoid value that acts as a log. As before, we’ll use a list of strings as our monoid. So, this should be the type of our new gcd' function:
gcd' :: Int -> Int -> Writer [String] Int
All that’s left now is to equip our function with log values. Here is the code:
import Control.Monad.Writer
gcd' :: Int -> Int -> Writer [String] Int
gcd' a b
| b == 0 = do
tell ["Finished with " ++ show a]
return a
| otherwise = do
tell [show a ++ " mod " ++ show b ++ " = " ++ show (a `mod` b)]
gcd' b (a `mod` b)
This function takes two normal Int values and returns a Writer [String] Int—that is, an Int that has a log context. In the case where b is 0, instead of just giving a as the result, we use a do expression to put together a Writer value as a result. First, we use tell to report that we’re finished, and then we use return to present a as the result of the do expression. Instead of this do expression, we could have also written this:
writer (a, ["Finished with " ++ show a])
However, I think the do expression is easier to read.
Next, we have the case when b isn’t 0. In this case, we log that we’re using mod to figure out the remainder of dividing a and b. Then the second line of the do expression just recursively calls gcd'. Remember that gcd' now ultimately returns a Writer value, so it’s perfectly valid that gcd' b (a `mod` b) is a line in a do expression.
Let’s try out our new gcd'. Its result is a Writer [String] Int value, and if we unwrap that from its newtype, we get a tuple. The first part of the tuple is the result. Let’s see if it’s okay:
ghci> fst $ runWriter (gcd' 8 3)
1
Good! Now what about the log? Because the log is a list of strings, let’s use mapM_ putStrLn to print those strings on the screen:
ghci> mapM_ putStrLn $ snd $ runWriter (gcd' 8 3)
8 mod 3 = 2
3 mod 2 = 1
2 mod 1 = 0
Finished with 1
I think it’s awesome how we were able to change our ordinary algorithm to one that reports what it does as it goes along. And we did this just by changing normal values to monadic values. We let the implementation