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
Writer
s 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 theempty
value from the typeclass. to fill theL
value 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 theWriterT
functions require aFunctor[F]
orMonad[F]
instance. However, Cats provides all the necessary instances for theId
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"""