Skip to content

Writer: Producing a Log

We discussed the Writer monad before. We used it to decorate the return values of functions with log messages that described how these values were produced. This is useful for debugging purposes, but it is not as general as it could be. Our current Writer monad is restricted to log messages of type String. What if we wanted to produce some system log that contains more structured records of events, not just text. In that case, we would want to change our log type from String to [LogRecord], for an appropriate LogRecord type. Our function composition then concatenates the lists of log records produced by the two functions being composed. But this still talks about concatenation, only now of arbitrary lists instead of strings—still not general enough. Remember that lists form a monoid. Instead of using concatenation to add to the log, we can have our log type be an arbitrary monoid, and then we combine log messages using monoid multiplication.

Thus, we want our Writer monad to be defined more generally as

newtype Writer w a = Writer { runWriter :: (a, w) }

where w is an arbitrary monoid. Our previous definition of Writer is equivalent to Writer String, with w = String. You should think about a computation with result type Writer w a as a computation that returns a value of type a but also some log of type w, where the term "log" is to be interpreted pretty broadly.

The assumption that w is a monoid is not part of the definition of the Writer type, but it is imposed as a constraint on the Monad instance for Writer, which relies on w being a monoid:

instance Monoid w => Monad (Writer w) where
    ...

Let's figure out what return and (>>=) look like for Writer w. These functions practically write themselves, given that we know nothing about w except that it's a monoid and thus has a unit element mempty and supports the monoid multiplication (<>).

If x :: a, return x needs to be a value of type Reader w a, which is really just a thin wrapper around a pair of type (a, w). Knowing nothing about the type a, the only choice we have is to use x as the first component of the pair. We have no way to create a different value of type a. What should the second component be? It needs to be a value of type w. Given that w is a monoid, we have one such value, mempty, and this is in fact the only value that is guaranteed to exist for every monoid. Thus, the only possible implementation of return is

return x = Writer (x, mempty)

Now (>>=). If x :: Writer w a and f :: a -> Writer w b, then both x and the return value of f are decorated with some values of type w. Let's call these values wx and wf. x >>= f should have the type Writer w b, that is, it should also be decorated with some value wxf of type w. There are only a few choices for wxf:

  • We could choose wxf = mempty. In this case, the decorations of wx and wf are completely ignored. Sequencing of functions that log their actions completely throws away the logs produced by these functions. Probably not what we want.
  • We could choose wxf = wx or wxf = wf. This would completely ignore wf or wx, again not what we want probably.
  • We could choose wxf = wx <> wf. This seems sensible and is in fact exactly what we did for our string-specific Writer monad. There, we concatenated the log messages produced by x and f, and strings form a monoid where the multiplication is concatenation. So this is a good candidate.
  • Finally, we chould choose wxf = wx <> wf <> wf or wxf = wx <> wf <> wx <> wf, and so on. But this would somehow duplicate the decorations of at least one of x or f. For example, if our monoid is String, then we're suddenly seeing x or f's log message more than once even though we ran x and f only once. This doesn't seem to make a lot of sense either.

So it seems that the most sensible choice is to choose wxf = wx <> wf. It also turns out that this is one of the only two choices that satisfy the monad laws. The other one is wxf = wf <> wx, but it isn't clear why we would want to combine the log messages in the opposite order from the one in which functions are executed. Thus, we get the following implementation of x >>= f:

x >>= f = Writer $ let (y, wx) = runWriter x
                       (z, wf) = runWriter (f y)
                   in  (z, wx <> wf)

We use runWriter to split x into its value y and its decoration wx. We apply f to y and use runWriter to split the result into f's return value and the decoration wf. The result of the whole computation is f's return value and the product wx <> wf of the two decorations. This captures both that x >>= f should express the idea of function application—z is the result of applying f to x's value—and that it should combine the decorations wx and wf.

Note the similarity to the implementation of (>=>) for the string-specific Writer. We simply replaced the string concatenation operator (++) with the more general monoid multiplication operator (<>). Our whole Monad instance for Writer w is

instance Monoid w => Monad (Writer w) where
    return x = Writer (x, mempty)
    x >>= f  = Writer $ let (y, wx) = runWriter x
                            (z, wf) = runWriter (f y)
                        in  (z, wx <> wf)

It would be an illuminating exercise at this point to verify that this definition satisfies the monad laws. Give it a try. As a bonus, verify that choosing the product wf <> wx as the decoration of x >>= f would also satisfy the monad laws.

Let's look at a simple example that uses a monoid other than the list monoid as decoration. We look at the same two functions double and add1 from before, but this time, we want to count how many function calls our program performs. This could be useful for profiling purposes. To this end, we use Sum as the monoid instead of String. Both double and add1 decorate their return value with Sum 1. If we call only double or add1, that's exactly one function call:

double :: Int -> Writer (Sum Int) Int
double x = Writer (2 * x, Sum 1)

add1 :: Int -> Writer (Sum Int) Int
add1 x = Writer (x + 1, Sum 1)

The monoid multiplication of the Sum monoid is addition, so combining the decorations of multiple functions ends up adding these decorations together, that is, we end up counting the number of function calls. Let's try it out:

GHCi
>>> :{
  | double, add1 :: Int -> Writer (Sum Int) Int
  | double x = writer (2 * x, Sum 1)
  | add1 x = writer (x + 1, Sum 1)
  | :}
>>> runWriter $ (double >=> add1) 10
(21,Sum {getSum = 2})

Given 10 as argument, double >=> add1 produces the result 21, and there were 2 calls of logging functions involved (double and add1).

You'll notice that our definitions of double and add1 in this GHCi session use the function writer instead of the data constructor Writer. That's because the Writer monad provided by Control.Monad.Writer isn't implemented exactly as we discussed in this section, even though it behaves exactly the same.

As we will discuss when talking about monad transformers, the real definition of Writer is as a type alias

type Writer w a = WriterT w Identity a

where WriterT is a monad transformer that can be used to equip any monad m with a log of type w:

newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }

You should also notice that I provided type signatures for double and add1, something I generally avoid in interactive GHCi sessions. Without these type signatures, our definitions ouf double and add1 would not compile:

GHCi
>>> double x = writer (2 * x, Sum 1)

<interactive>:12:1: error:
    • Non type-variable argument
        in the constraint: MonadWriter (Sum a1) m
      (Use FlexibleContexts to permit this)
    • When checking the inferred type
        double :: forall {a1} {m :: * -> *} {a2}.
                  (MonadWriter (Sum a1) m, Num a2, Num a1) =>
                  a2 -> m a2

The culprit is writer, which has the type

writer :: MonadWriter w m => (a, w) -> m a

This says that writer turns any pair (a, w) into a computation in any monad m that is an instance of MonadWriter w. MonadWriter w is a type class we will also discuss when discussing monad transformers. A monad is an instance of this class if it has a log of type w. Writer w is an instance of this class, as is WriterT w m for any monad m, and we can also define completely custom-built monads that we can make instances of this class.

writer not specifying the monad it uses to decorate its return value isn't a problem in itself, but it becomes a problem because, given that we pass (2 * x, Sum 1) as an argument to writer, GHCi deduces that double must have the type

double :: (MonadWriter (Sum a1) m, Num a2, Num a1) => a2 -> m a2

The error message complains about the Sum a1 part. Standard Haskell only allows constraints of the form Class a b c ..., where a, b, c, ... are type variables. Sum a1 clearly isn't a variable, even though it does involve one (a1). By providing a concrete type for double and add1, we sidestep the whole mess. The other option would have been to Use FlexibleContexts, as suggested in the error message. This permits constraints such as MonadWriter (Sum a1) m. To enable this language extension, we use

GHCi
>>> :set -XFlexibleContexts

And now our definitions of double and add1 are accepted without type signatures:

GHCi
>>> double x = writer (2 * x, Sum 1)
>>> add1 x = writer (x + 1, Sum 1)
>>> runWriter $ (double >=> add1) 10
(21,Sum {getSum = 2})

Logging and Inspecting the Log

As with any monad, if we use do-notation, we don't have direct access to the decoration provided by the monad. For the Reader r monad, we had functions ask and asks to gain access to the context of type r. Similarly, the Control.Monad.Writer module provides us with functions that allow us to manipulate the log from within do-blocks.

The only one we discuss here is

tell :: w -> Writer w ()
tell msg = Writer ((), msg)

This function produces the log message msg and does not return anything useful. Remember, () is our void type. It has exactly one value, (), but that value carries no information. If we sequence tell msg with other statements in a do-block, then the Writer monad's (>>=) operator combines the log messages produced by these statements. Thus, tell msg simply has the effect of adding msg to the log produced by the whole computation. This allows us to write our string versions of double and add1 functions more pleasantly:

double :: Int -> Writer String Int
double x = do
    tell   $ "Doubling " ++ show x ++ "\n"
    return $ 2 * x

add1 :: Int -> Writer String Int
add1 x = do
    tell   $ "Adding 1 to " ++ show x ++ "\n"
    return $ x + 1

This says that double x should add the log message "Doubling " ++ show x to the log and return the result 2 * x. And indeed, it does exactly this. Since return produces an empty log message, concatenating it with the message produced by tell just leaves the message produced by tell. The return value of the function is the one produced by return, which is 2 * x. The definition of add1 is analogous. Both definitions read like a sequence of things that double and add1 should do. As expected, these two implementations behave exactly like the ones we had before:

GHCi
>>> :{
  | double :: Int -> Writer String Int
  | double x = do
  |     tell   $ "Doubling " ++ show x ++ "\n"
  |     return $ 2 * x
  |
  | add1 :: Int -> Writer String Int
  | add1 x = do
  |     tell   $ "Adding 1 to " ++ show x ++ "\n"
  |     return $ x + 1
  | :}
>>> runWriter $ (double >=> add1) 10
(21,"Doubling 10\nAdding 1 to 20\n")

Once again, we had to provide type signatures here to avoid type constraints on double and add1 that involve non-type variable arguments. If you are still in the same GHCi session where you enabled FlexibleContexts, then this code compiles without providing type signatures.

The complete list of functions in Control.Monad.Writer is beyond the scope of our discussion, and some of them seem rather esoteric. If you're interested, search Control.Monad.Writer on Hoogle and see which functions there are.

Collecting a Writer's Works

For the Reader monad, we had our function runReader that turns a computation f of type Reader r a into a plain old function of type r -> a. We can apply this function to a context of type r and obtain the value of type a that is produced by f when run with this context.

Similarly, if we have a computation f :: Writer w a, then we think of f as producing a log of type w along with a value of type a. The runWriter function allows us to turn f into a plain old value: runWriter f :: (a, w). Thus, we can define a function

g :: b -> (a, w)
g x = runWriter f
  where
    f = -- Implement f as a computation in the Writer monad that produces its result from x

Nothing in g's type indicates that it uses the Writer monad for its implementation. Once again, the Writer monad is merely a convenience. It does not add any capabilities that we couldn't have achieved using pure functions throughout.

We have a second function to "run" a computation in the Writer monad:

execWriter :: Writer w a -> w
execWriter (Writer (_, w)) = w

execWriter (f x) returns only the log produced by f but discards f's return value. Sometimes, the log is all we care about. As a silly but short example, we could implement the map function for lists using execWriter:

map :: (a -> b) -> [a] -> [b]
map f = execWriter . go
  where
    go []     = return ()
    go (x:xs) = do
        tell [f x]
        go xs
GHCi
>>> :{
  | map :: (a -> b) -> [a] -> [b]
  | map f = execWriter . go
  |   where
  |     go []     = return ()
  |     go (x:xs) = do
  |         tell [f x]
  |         go xs
  | :}
>>> map (* 2) [1..5]
[2,4,6,8,10]

How does this work? The type of our function go is

go :: [a] -> Writer [b] ()

It doesn't return anything useful but produces a log that is a list of values of type b. When running go ys, for some list ys :: [a], we start with the empty log. If ys is empty, then go ys simply returns (). In particular, it does not add anything to the log. If ys = x:xs, then go ys adds the singleton list [f x] to the log. Since the monoid multiplication for lists is list concatenation, we should think of this as adding the value f x to the list that is the log of our computation. We then call go xs recursively to produce the remainder of the log. Once we reach the end of the input list, we have added the value f x to the log for every element x in the input list. By running this computation using execWriter, we discard the return value of go, of type (), and retrieve only the log produced by go, which is exactly the list of all these values f x, that is, the result of mapping f over the argument list of go.