Writer Monad

Scenario

Imagine you have a function, f which takes in an argument a, produces a result b.

Now, you want log the input in the function.

In languages like Javascript where “functions” can produce side-effects, you could probably capture it in a state variable:

let log = "" // We accumulate the logs here

function addOne (n) {
  log = log + `Adding one to ${n}`
  return n + 1
}

In haskell since functions have to be pure, you could do something like this:

type Log = String

addOne :: Int -> (Log, Int) -- Embelishing the function to the accumulated log along with the result
addOne n = ("Adding one to " <> show n, n + 1)

Continuing on, let’s invoke this function:

addedOne :: (Log, Int)
addedOne = addOne 1

Now suppose we want to use addOne on addedOne again. We have to lift addOne’s first argument into the (,) Log context..

addOne' :: (Log, Int) -> (Log, Int)
addOne' (log, n) = let (log2, n2) = addOne n
                   in (log ++ log2, n2)

Notice the difference in the type signature for addOne and addOne’

Is there some way we can abstract some of this away?

If we observe log ++ log2 it seems like a process we will repeat each time, combining logs whenever we call an adding function.

We also know the actual logic is in addOne, not addOne' which is a wrapper around addOne, allowing us to combine the logs.

By abstracting these patterns, we don’t have to write boilerplate code to add the logs each time.

One abstraction we can have is a higher order function which can compose addOnes:

f :: (Int -> (Log, Int)) -> (Int -> (Log, Int)) -> (Int -> (Log, Int))

We can observe this resembles the “fish operator”:

(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)

We can then try to implement a monadic type here:

data WriterL a = WriterL (Log, a)

class Monad WriterL where 
    return a = ("", a)
    f1 >=> f2 = \a -> let (l1, b) = f1 a
                          (l2, c) = f2 b
                      in  (l1 <> l2, c)
main :: WriterL Int
main = do
   value <- genValues
   addOne value
   
main =
  genValues >>=
  addOne
     
genValues :: WriterL Int
genValues = ("1", 1)

("1AddOne ...", 2)

We can then use >=> to compose addOnes, composing the underlying functionality and combining the contexts, in this case the logs.

We do realize however, that a -> m b means that the composed function only looks at a.

As such, its treatment of the previous context in m a is fixed, when we defined the >=> operator. It monadically adds the logs.

Hence, we would not be able to view/read the logs along the way if we wanted to. The definition has made the log implicit.

Let’s explore another construct to do read, the Reader Monad.