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 ofwx
andwf
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
orwxf = wf
. This would completely ignorewf
orwx
, 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-specificWriter
monad. There, we concatenated the log messages produced byx
andf
, and strings form a monoid where the multiplication is concatenation. So this is a good candidate. - Finally, we chould choose
wxf = wx <> wf <> wf
orwxf = wx <> wf <> wx <> wf
, and so on. But this would somehow duplicate the decorations of at least one ofx
orf
. For example, if our monoid isString
, then we're suddenly seeingx
orf
's log message more than once even though we ranx
andf
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:
>>> :{
| 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:
>>> 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
>>> :set -XFlexibleContexts
And now our definitions of double
and add1
are accepted without type
signatures:
>>> 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:
>>> :{
| 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
>>> :{
| 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
.