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:
Monoid[L], since it uses theemptyvalue from the typeclass. to fill theLvalue not specified in the input.Applicative[F]to modify the inner value.
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 theWriterTfunctions require aFunctor[F]orMonad[F]instance. However, Cats provides all the necessary instances for theIdtype, 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"""