Writer

API Documentation: WriterT

The Writer[L, A] datatype represents a computation that produces a tuple containing a value of type L and one of type A. Usually, the value L represents a description of the computation. A typical example of an L value could be a logging String and that's why from now on we will refer to it as the Logging side of the datatype. Meanwhile, the value A is the actual output of the computation.

The main features that Writer provides are:

The flexibility regarding Log value management. It can be modified in multiple ways. See the Operations section

When two functions are composed together, e.g. using flatMap, the logs of both functions will be combined using an implicit Semigroup.

Operations

The Writer datatype provides a set of functions that are similar to the ones from the Monad typeclass. In fact, they share the same name and the same signature, but have an additional requirement of a Semigroup[L] that allows the log merging.

map effects only the value, keeping the log side untouched. Plus, here we show run that just unwrap the datatype, returning its content.

import cats.data.Writer
import cats.instances._

val mapExample = Writer("map Example", 1).map(_ + 1)
// mapExample: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("map Example", 2)
// )

mapExample.run
// res0: cats.package.Id[(String, Int)] = ("map Example", 2)

ap allows applying a function, wrapped into a Writer. It works exactly like the Applicative as expected, but notice how the logs are combined using the Semigroup[String].

val apExampleValue = Writer("ap value", 10)
// apExampleValue: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("ap value", 10)
// )
val apExampleFunc = Writer("ap function ", (i: Int) => i % 7)
// apExampleFunc: cats.data.WriterT[cats.package.Id, String, Int => Int] = WriterT(
//   run = ("ap function ", <function1>)
// )

apExampleValue.ap(apExampleFunc).run
// res1: cats.package.Id[(String, Int)] = ("ap function ap value", 3)

Same thing for flatMap

val flatMapExample1 = Writer("flatmap value", 5)
// flatMapExample1: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("flatmap value", 5)
// )
val flatMapExample2 = (x: Int) => Writer("flatmap function ", x * x)
// flatMapExample2: Int => cats.data.WriterT[cats.package.Id, String, Int] = <function1>

flatMapExample1.flatMap(flatMapExample2).run
// res2: cats.package.Id[(String, Int)] = (
//   "flatmap valueflatmap function ",
//   25
// )

// We can use the for comprehension as well
val flatMapForResult = for {
    value <- Writer("flatmap value", 5)
    result <- flatMapExample2(value)
} yield result
// flatMapForResult: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("flatmap valueflatmap function ", 25)
// )

flatMapForResult.run
// res3: cats.package.Id[(String, Int)] = (
//   "flatmap valueflatmap function ",
//   25
// )

Apart from those, Writer comes with some specific functions to manage the log side of the computation:

tell : Append a value to the log side. It requires a Semigroup[L].

swap : Exchange the two values of the Writer.

reset : Delete the log side. It requires a Monoid[L] since it uses the empty value of the monoid.

value : Returns only the value of the Writer

listen : Transform the value of the Writer to a tuple containing the current value and the current log.

val tellExample = Writer("tell example", 1).tell("log append")
// tellExample: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("tell examplelog append", 1)
// )

tellExample.run
// res4: cats.package.Id[(String, Int)] = ("tell examplelog append", 1)

val swapExample = Writer("new value", "new log").swap
// swapExample: cats.data.WriterT[cats.package.Id, String, String] = WriterT(
//   run = ("new log", "new value")
// )

swapExample.run
// res5: cats.package.Id[(String, String)] = ("new log", "new value")

val resetExample = Writer("long log to discard", 42).reset
// resetExample: cats.data.WriterT[cats.package.Id, String, Int] = WriterT(
//   run = ("", 42)
// )

resetExample.run
// res6: cats.package.Id[(String, Int)] = ("", 42)

val valueExample = Writer("some log", 55).value
// valueExample: cats.package.Id[Int] = 55

valueExample
// res7: cats.package.Id[Int] = 55

val listenExample = Writer("listen log", 10).listen
// listenExample: cats.data.WriterT[cats.package.Id, String, (Int, String)] = WriterT(
//   run = ("listen log", (10, "listen log"))
// )

listenExample.run
// res8: cats.package.Id[(String, (Int, String))] = (
//   "listen log",
//   (10, "listen log")
// )

Definition

If we go looking at how Writer is actually defined, we will see it is just a type alias:

import cats.data.WriterT
import cats.Id
// cats/data/package.scala
type Writer[L, V] = WriterT[Id, L, V]

So, all the Operations defined in the previous section are actually coming from the WriterT datatype

Most of the WriterT functions require a Functor[F] or Monad[F] instance. However, Cats provides all the necessary instances for the Id type, therefore we don't have to worry about them.

Example

The example showed in here is taken from the Rosetta Code site. It simply applies a bunch of Math operations, logging each one of them.

import cats.data.Writer
import scala.math.sqrt

val writer1: Writer[String, Double] = Writer.value[String, Double](5.0).tell("Initial value ")
val writer2: Writer[String, Double => Double] = Writer("sqrt ", (i: Double) => sqrt(i))
val writer3: Double => Writer[String, Double] = (x: Double) => Writer("add 1 ", x + 1)
val writer4: Writer[String, Double => Double] = Writer("divided by 2 ", (x: Double) => x / 2)

val writer5: Writer[String, Double => Double] = Writer[String, Double => Double](writer3(0).written,(x: Double) => writer3(x).value)
// Pay attention on the ordering of the logs

writer1
  .ap(writer2)
  .flatMap(writer3(_))
  .ap(writer4)
  .map(_.toString)
  .run
// res10: cats.package.Id[(String, String)] = (
//   "divided by 2 sqrt Initial value add 1 ",
//   "1.618033988749895"
// )

import cats.syntax.compose._

(for {
    initialValue <- writer1
    sqrt <- writer2
    addOne <- writer5
    divideBy2 <- writer4
    } yield (sqrt >>> addOne >>> divideBy2)(initialValue)
).run
// res11: cats.package.Id[(String, Double)] = (
//   "Initial value sqrt add 1 divided by 2 ",
//   1.618033988749895
// )

If you are interested in logging solutions, we recommend the library log4cats