Writer monad
TweetThe Writer monad represents computations which produce a stream of data in addition to the computed values. It is commonly used by code generators to emit code.
transformers provides both the strict and lazy versions of WriterT
monad transformer. The definition of bind operator >>=
reveals how the Writer monad works.
instance (Monoid w, Monad m) => Monad (WriterT w m) where
return a = writer (a, mempty)
m >>= k = WriterT $ do
(a, w) <- runWriterT m
(b, w') <- runWriterT (k a)
return (b, w `mappend` w')
runWriterT
returns a pair whose second element is the output to accumulate. Because the output value is a Monoid
instance, we can merge two outputs w
and w'
using mappend
and return the combined output.
Here is a simple example of the Writer monad. It accumulates LogEntry
s in a list. (CAUTION: Do not use WriterT
for plain logging in real world applications. It unnecessarily keeps the entire logs in memory. I recommend fast-logger for logging.)
import Control.Monad
import Control.Monad.Trans.Writer.Strict
data LogEntry = LogEntry { msg::String }
deriving (Eq, Show)
calc :: Writer [LogEntry] Integer
calc = do
output "start"
let x = sum [1..10000000]
output (show x)
output "done"
return x
output :: String -> Writer [LogEntry] ()
output x = tell [LogEntry x]
test = mapM_ print $ execWriter calc
The code looks innocuous, but its performance deteriorates when the accumulated log gets bigger because the Monoid
instance of []
uses (++)
to append two lists and the concatenations are left-nested.
do { tell [1]; tell [2]; tell [3]; tell[4]; tell [5] }
=>
(((([1] ++ [2]) ++ [3]) ++ [4]) ++ [5])
(++)
is known to perform poorly when applications of (++)
are left-nested.
Difference List
One well-known solution is to use the difference list instead of an ordinary list. DList
provides O(1) append
and snoc
operations on lists. Demystifying DList explains how DList
works in details.
The code is almost the same except we replaced [LogEntry]
with DList LogEntry
, but it scales well as the accumulated log gets bigger.
import Data.DList
calc :: Writer (DList LogEntry) Integer
calc = ...
output :: String -> Writer (DList LogEntry) ()
output x = tell (singleton (LogEntry x))
test = mapM_ print $ toList (execWriter calc)
Endo
Another option is to use Endo
wrapper from Data.Monoid
. It is an endomorphism from type a
to a
.
newtype Endo a = Endo { appEndo :: a -> a }
deriving (Generic)
Surprisingly, it is an instance of Monoid
. mempty
is the identity function and mappend
is the composition of two functions.
instance Monoid (Endo a) where
mempty = Endo id
Endo f `mappend` Endo g = Endo (f . g)
But how can I output a log? We need a function of type [LogEntry] -> [LogEntry]
to make an Endo
value. The trick is to create a section ([LogEntry x]<>)
which prepends a log entry to the list.
calc :: Writer (Endo [LogEntry]) Integer
calc = ...
output :: String -> Writer (Endo [LogEntry]) ()
output x = tell $ Endo ([LogEntry x]<>)
test = mapM_ print $ appEndo (execWriter calc) []
But why does this use of Endo
perform well? To see why, we need to see how the following code is actually evaluated.
do { tell [1]; tell [2]; tell [3]; tell[4]; tell [5] }
is translated to
([1]++) . ([2]++) . ([3]++) . ([4]++) . ([5]++)
This is a composition of functions whose type is [Int] -> [Int]
. We can obtain the final result by applying []
.
([1]++) . ([2]++) . ([3]++) . ([4]++) . ([5]++) $ []
=>
[1] ++ ([2] ++ ([3] ++ ([4] ++ ([5] ++ []))))
We can see that (++)
operators are right-nested.
This also explains why DList
in the previous section performs well because DList
is just Endo
specialized to lists.
newtype DList a = DL { unDL :: [a] -> [a] }
instance Monoid (DList a) where
mempty = DL id
mappend xs ys = DL (unDL xs . unDL ys)
State Monad
It is possible to implement the Writer monad in terms of the State monad. We can store the accumulated logs in the state and update it by appending a new log.
import Control.Monad.Trans.State
import Data.Monoid ((<>))
calc :: State [LogEntry] Integer
calc = ...
output :: String -> State [LogEntry] ()
output x = modify (<> [LogEntry x])
test = mapM_ print $ execState calc []
Unfortunately, this version has the same performance issue with the initial version because applications of (++)
are left-nested.
But there is a magical trick that can change this situation.
Backward State Monad
The section “2.8 Variation six: Backwards state” of Philip Wadler’s The essence of functional programming briefly mentions the Backwards state monad (also known as reverse state monad). This is a strange variant of the State monad where the state is propagated backward.
newtype RState s a = RState { runRState :: s -> (a,s) }
instance Monad (RState s) where
return x = RState $ (,) x
RState sf >>= f = RState $ \s ->
let (a,s'') = sf s'
(b,s') = runRState (f a) s
in (b,s'')
rget = RState $ \s -> (s,s)
rmodify f = RState $ \s -> ((),f s)
rput = rmodify . const
execRState f s = snd (runRState f s)
In the definition of >>=
, the state s
is passed to the second expression and its result s'
is passed back to the first expression. This seems impossible because two expressions are mutually recursive, but Haskell’s lazy evaluation makes it possible. In the backward state monad, rget
reads the state from the future!
With this in mind, we can implement the Writer monad by prepending the log to the state. Because the state contains all the future logs, we can simply prepend our log to it.
calc :: RState [LogEntry] Integer
calc = ...
output :: String -> RState [LogEntry] ()
output x = rmodify ([LogEntry x]<>)
test = mapM_ print $ execRState calc []
Applications of (++)
are right-nested because logs are accumulated backward from the end.
Readers who would like to know more about the backward state monads are referred to:
- Mindfuck: The Reverse State Monad shows how to compute the fibonacci number using the reverse state monad.
- tardis package - a combination of both a forwards and a backwards state transformer.