WriterT

API Documentation: WriterT

WriterT[F[_], L, V] is a type wrapper on an F[(L, V)]. Speaking technically, it is a monad transformer for Writer, but you don't need to know what that means for it to be useful.

Composition

WriterT can be more convenient to work with than using F[Writer[L, V]] directly, because it exposes operations that allow you to work with the values of the inner Writer (L and V) abstracting both the F and Writer.

For example, map allow you to transform the inner V value, getting back a WriterT that wraps around it.

import cats.data.{WriterT, Writer}

WriterT[Option, String, Int](Some(("value", 10))).map(x => x * x)
// res0: WriterT[Option, String, Int] = WriterT(
//   run = Some(value = ("value", 100))
// )

Plus, when composing multiple WriterT computations, those will be composed following the same behaviour of a Writer and the generic F. Let's see two examples with Option and Either: if one of the computations has a None or a Left, the whole computation will return a None or a Left since the way the two types compose typically behaves that way. Moreover, when the computation succeed, the logging side of the Writers will be combined.

val optionWriterT1 : WriterT[Option, String, Int] = WriterT(Some(("writerT value 1", 123)))
val optionWriterT2 : WriterT[Option, String, Int] = WriterT(Some(("writerT value 1", 123)))
val optionWriterT3 : WriterT[Option, String, Int] = WriterT.valueT(None)


val eitherWriterT1 : WriterT[Either[String, *], String, Int] = WriterT(Right(("writerT value 1", 123)))
val eitherWriterT2 : WriterT[Either[String, *], String, Int] = WriterT(Right(("writerT value 1", 123)))
val eitherWriterT3 : WriterT[Either[String, *], String, Int] = WriterT.valueT(Left("error!!!"))
// This returns a Some since both are Some
for {
    v1 <- optionWriterT1
    v2 <- optionWriterT2
} yield v1 + v2
// res1: WriterT[Option, String, Int] = WriterT(
//   run = Some(value = ("writerT value 1writerT value 1", 246))
// )

// This returns a None since one is a None
for {
    v1 <- optionWriterT1
    v2 <- optionWriterT2
    v3 <- optionWriterT3
} yield v1 + v2 + v3
// res2: WriterT[Option, String, Int] = WriterT(run = None)

// This returns a Right since both are Right
for {
    v1 <- eitherWriterT1
    v2 <- eitherWriterT2
} yield v1 + v2
// res3: WriterT[[β$0$]Either[String, β$0$], String, Int] = WriterT(
//   run = Right(value = ("writerT value 1writerT value 1", 246))
// )

// This returns a Left since one is a Left
for {
    v1 <- eitherWriterT1
    v2 <- eitherWriterT2
    v3 <- eitherWriterT3
} yield v1 + v2 + v3
// res4: WriterT[[β$0$]Either[String, β$0$], String, Int] = WriterT(
//   run = Left(value = "error!!!")
// )

Just for completeness, we can have a look at the same example, but with Validated since it as a slightly different behaviour than Either. Instead of short-circuiting when the first error is encountered, Validated will accumulate all the errors. In the following example, you can see how this behaviour is respected when Validated is wrapped as the F type of a WriterT. In addition, notice how flatMap and for comprehension can't be used in this case, since Validated only extends Applicative, but not Monad.

import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
import cats.syntax.all._

val validatedWriterT1 : WriterT[Validated[String, *], String, Int] = WriterT(Valid(("writerT value 1", 123)))
val validatedWriterT2 : WriterT[Validated[String, *], String, Int] = WriterT(Valid(("writerT value 1", 123)))
val validatedWriterT3 : WriterT[Validated[String, *], String, Int] =
WriterT(Invalid("error 1!!!") : Validated[String, (String, Int)])
val validatedWriterT4 : WriterT[Validated[String, *], String, Int] = WriterT(Invalid("error 2!!!"): Validated[String, (String, Int)])
// This returns a Right since both are Right
(validatedWriterT1,
validatedWriterT2
).mapN((v1, v2) => v1 + v2)
// res5: WriterT[[β$4$]Validated[String, β$4$], String, Int] = WriterT(
//   run = Valid(a = ("writerT value 1writerT value 1", 246))
// )

// This returns a Left since there are several Left
(validatedWriterT1,
 validatedWriterT2,
 validatedWriterT3,
 validatedWriterT4
).mapN((v1, v2, v3, v4) => v1 + v2 + v3 + v4)
// res6: WriterT[[β$6$]Validated[String, β$6$], String, Int] = WriterT(
//   run = Invalid(e = "error 1!!!error 2!!!")
// )

Construct a WriterT

A WriterT can be constructed in different ways. Here is the list of the main available constructors with a brief explanation and an example.

WriterT[F[_], L, V](run: F[(L, V)]) : This is the constructor of the datatype itself. It just builds the type starting from the full wrapped value.

// Here we use Option as our F[_]
  val value : Option[(String, Int)] = Some(("value", 123))
// value: Option[(String, Int)] = Some(value = ("value", 123))
  WriterT(value)
// res7: WriterT[Option, String, Int] = WriterT(
//   run = Some(value = ("value", 123))
// )

liftF[F[_], L, V](fv: F[V])(implicit monoidL: Monoid[L], F: Applicative[F]): WriterT[F, L, V] : This function allows you to build the datatype starting from the value V wrapped into an F. Notice how it requires:

import cats.instances.option._

  val value : Option[Int] = Some(123)
// value: Option[Int] = Some(value = 123)

  WriterT.liftF[Option, String, Int](value)
// res8: WriterT[Option, String, Int] = WriterT(run = Some(value = ("", 123)))

put[F[_], L, V](v: V)(l: L)(implicit applicativeF: Applicative[F]): WriterT[F, L, V] : As soon as there is an Applicative instance of F, this function creates the datatype starting from the inner Writer's values.

WriterT.put[Option, String, Int](123)("initial value")
// res9: WriterT[Option, String, Int] = WriterT(
//   run = Some(value = ("initial value", 123))
// )

putT[F[_], L, V](vf: F[V])(l: L)(implicit functorF: Functor[F]): WriterT[F, L, V] : Exactly as put, but the value V is already wrapped into F

WriterT.putT[Option, String, Int](Some(123))("initial value")
// res10: WriterT[Option, String, Int] = WriterT(
//   run = Some(value = ("initial value", 123))
// )

Operations

In the Writer definition section, we showed how it is actually a WriterT. Therefore, all the operations described into Writer operations are valid for WriterT as well.

The only aspect we want to remark here is the following sentence from Writer's page:

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.

In the case of WriterT, the user needs to ensure the required instances are present. Cats still provide a lot of default instances, so there's a high chance you could find what you are searching for with the right import.

Example

As an example, we can consider a simple naive console application that pings multiple HTTP well-known services and collect the time spent in each call, returning the total time of the whole execution at the end. We will simulate the calls by successful Future values.

Using WriterT we can log each step of our application, compute, the time and work within the Future effect.

import cats.data.WriterT

import scala.concurrent.{Await, Future}
import scala.concurrent.duration.Duration
import scala.concurrent.ExecutionContext.Implicits.global

// Mocked HTTP calls
def pingService1() : Future[Int] = Future.successful(100)
def pingService2() : Future[Int] = Future.successful(200)
def pingService3() : Future[Int] = Future.successful(50)
def pingService4() : Future[Int] = Future.successful(75)

def pingToWriterT(ping: Future[Int], serviceName: String) : WriterT[Future, String, Int] =
    WriterT.valueT[Future, String, Int](ping)
        .tell(s"ping to $serviceName ")
        .flatMap(pingTime => WriterT.put(pingTime)(s"took $pingTime \n"))

val resultWriterT: WriterT[Future, String, Int] = for {
    ping1 <- pingToWriterT(pingService1(), "service #1")
    ping2 <- pingToWriterT(pingService2(), "service #2")
    ping3 <- pingToWriterT(pingService3(), "service #3")
    ping4 <- pingToWriterT(pingService4(), "service #4")
  } yield ping1 + ping2 + ping3 + ping4

val resultFuture: Future[String] = resultWriterT.run.map {
    case (log: String, totalTime: Int) => s"$log> Total time: $totalTime"
}

And the final result as expected:

Await.result(resultFuture, Duration.Inf)
// res11: String = """ping to service #1 took 100 
// ping to service #2 took 200 
// ping to service #3 took 50 
// ping to service #4 took 75 
// > Total time: 425"""