Example: The Writer Monad
When trying to diagnose faults in a system, it's often the case that a log of what functions have been called, as well as what their inputs and outputs were, would be helpful.
Imagine that we had two functions we wanted to debug,
both of type int -> int
. For example:
let inc x = x + 1
let dec x = x - 1
(Ok, those are really simple functions; we probably don't need any help debugging them. But imagine they compute something far more complicated, like encryptions or decryptions of integers.)
One way to keep a log of function calls would be to augment each function to return a pair: the integer value the function would normally return, as well as a string containing a log message. For example:
let inc_log x = x + 1, Printf.sprintf "Called inc on %i; " x
let dec_log x = x - 1, Printf.sprintf "Called dec on %i; " x
But that changes the return type of both functions, which makes it hard to compose the functions. Previously, we could have written code such as
let id x = dec (inc x)
or even better
let id x = x |> inc |> dec
or even better still, using the composition operator >>
,
let (>>) f g x = x |> f |> g
let id = inc >> dec
and that would have worked just fine. But trying to do the same thing with the loggable versions of the functions produces a type-checking error:
let id = inc_log >> dec_log (* error *)
That's because inc_log x
would be a pair, but dec_log
expects simply
an integer as input.
We could code up an upgraded version of dec_log
that is able to
take a pair as input:
let dec_log_upgraded (x, s) =
x - 1, Printf.sprintf "Called dec on %i; " s
let id x = x |> inc_log |> dec_log_upgraded
That works fine, but we also will need to code up a similar
upgraded version of f_log
if we ever want to call them
in reverse order, e.g., let id = dec_log >> inc_log
.
So we have to write:
let inc_log_upgraded (x, s) =
x + 1, Printf.sprintf "Called inc on %i; " x
let id = dec_log >> inc_log_upgraded
And at this point we've duplicated far too much code. The
implementations of inc
and dec
are duplicated inside
both inc_log
and dec_log
, as well as inside both upgraded
versions of the functions. And both the upgrades duplicate
the code for concatenating log messages together. The
more functions we want to make loggable, the worse
this duplication is going to become!
So, let's start over, and factor out a couple helper functions. The first helper calls a function and produces a log message:
let log (name : string) (f : int -> int) : int -> int * string =
fun x -> (f x, Printf.sprintf "Called %s on %i; " name x)
The second helper produces a logging function
of type 'a * string -> 'b * string
out of a non-loggable function
let loggable (name : string) (f : int -> int) : int * string -> int * string =
fun (x, s1) ->
let (y, s2) = log name f x in
(y, s1 ^ s2)
Using those helpers, we can implement the logging versions of our functions without any duplication of code involving pairs or pattern matching or string concatenation:
let inc' : int * string -> int * string =
loggable "inc" inc
let dec' : int * string -> int * string =
loggable "dec" dec
let id' : int * string -> int * string =
inc' >> dec'
Here's an example usage:
# id' (5, "");;
- : int * string = (5, "Called inc on 5; Called dec on 6; ")
Notice how it's inconvenient to call our loggable functions on integers, since we have to pair the integer with a string. So let's write one more function to help with that by pairing an integer with the empty log:
let e x = (x, "")
And now we can write id' (e 5)
instead of id' (5, "")
.
Where's the Monad?
The work we just did was to take functions on integers and tranform them into functions on integers paired with log messages. We can think of these "upgraded" functions as computations that log. They produce metaphorical boxes, and those boxes contain function outputs as well as log messages.
There were two fundamental ideas in the code we just wrote, which
correspond to the monad operations of return
and bind
.
The first was upgrading a value from int
to int * string
by pairing
it with the empty string. That's what e
does. We could rename it
return
:
let return (x : int) : int * string =
(x, "")
This function has the trivial effect of putting a value into the metaphorical box along with the empty log message.
The second idea was factoring out code to handle pattern matching against pairs and string concatenation. Here's that idea expressed as its own function:
let (>>=) (m : int * string) (f : int -> int * string) : int * string =
let (x, s1) = m in
let (y, s2) = f x in
(y, s1 ^ s2)
Using return
and >>=
, we can re-implement loggable
, such that no pairs
or pattern matching are ever used in its body:
let loggable (name : string) (f : int -> int) : int * string -> int * string =
fun m ->
m >>= fun x ->
log name f x >>= fun y ->
return y
The Writer Monad
The monad we just discovered is usually called the writer monad (as in, "additionally writing to a log or string"). Here's an implementation of the monad signature for it:
module Writer : Monad = struct
type 'a t = 'a * string
let return x = (x, "")
let (>>=) m f =
let (x, s1) = m in
let (y, s2) = f x in
(y, s1 ^ s2)
end
As we saw with the maybe monad, these are the same implementations of
return
and >>=
as we invented above, but without the type
annotations to force them to work only on integers. Indeed, we never
needed those annotations; they just helped make the code above a little
clearer.
It's debatable which version of loggable
is easier to read. Certainly
you need to be comfortable with the monadic style of programming
to appreciate the version of it that uses >>=
. But if you were developing
a much larger code base (i.e., with more functions involving paired
strings than just loggable
), using the >>=
operator is likely to
be a good choice: it means the code you write can concentrate on the
'a
in the type 'a Writer.t
instead of on the strings. In other
words, the writer monad will take care of the strings for you, as long
as you use return
and >>=
.