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