Explore “herding cats”: Writer

3 minute read

In that post we will check out Writer of “herding cats” series by eed3si9n.

Writer

Scala with cats says: cats.data.Writer is a monad that lets us carry a log along with a computation. We can use it to record messages, errors, or additional data about a computation, and extract the log alongside the final result.

Typing

Let’s steal idea of Logged type from the book:

type Logged[A] = Writer[Vector[String], A]

On the left side we have our structure to keep logs. On the right we have our A value. Hence, we can fix the type to persist log and sole generic will be our A type for values.

Under the hood we need Monoid for log lines. Reasoning for having full-featured Monoid is necessity to make logging optional and produce computation results without any logs at all. Monoid[L].empty helps with that demand. And we need Applicative[F] to pack values.

def liftF[F[_], L, V](
  fv: F[V]
)(
  implicit monoidL: Monoid[L], F: Applicative[F]
): WriterT[F, L, V] =
    WriterT(F.map(fv)(v => (monoidL.empty, v)))

Writer as WriterT parametrized with Id

Yo can notice that we started to deal with WriterT[F, L, V] instead of Writer. Writer is an alias for WriterT with fixed Id data type.

type Writer[L, V] = WriterT[Id, L, V]

API

Run

We have run that “unpack” WriterT into tuple.

123.pure[Logged].run === (Vector(), 123)
Map

map allows tranformation of value.

Writer("example", 2).map(_ * 2).run === ("example", 4)
Ap

You can apply lifted function to your value with ap.

val multiplyByTwo: Writer[Vector[String], Int => Int] =
  Writer(Vector("multiplied by two"), _ * 2)

val (logs, value) = Writer(Vector("initial state"), 2).ap(multiplyByTwo).run

logs === Vector("multiplied by two", "initial state")
value === 4

Usage

Writer is not something you can observe really often.

Practical example is implementations of loggers for tests.

import org.typelevel.log4cats.extras.WriterLogger

val logger = WriterLogger()
logger.info("Hi, I'm message")

val logged: WriterT[Id, List[LogMessage], Unit] = logger.info("Hi, I'm message")
val message = logged.run._1.head

message.level === Info
message.message === "Hi, I'm message"

valskalla/odin logger also provides WriterTLogger.

Note by SystemFw (Typelevel/cats Discord):

The problem with logging in writer is that you will need WriterT of IO, which will fail to log on error