How do you use scalaz.WriterT for logging?
type OptionLogger[A] = WriterT[Option, NonEmptyList[String], A]
val two: OptionLogger[Int] = WriterT.put(2.some)("The number two".pure[NonEmptyList])
val hundred: OptionLogger[Int] = WriterT.put(100.some)("One hundred".pure[NonEmptyList])
val twoHundred = for {
a <- two
b <- hundred
} yield a * b
twoHundred.value must be equalTo(200.some)
val log = twoHundred.written map { _.list } getOrElse List() mkString(" ")
log must be equalTo("The number two One hundred")
About monad transformers
This is a very short introduction. You may find more information on haskellwiki or this great slide by @jrwest.
Monads don't compose, meaning that if you have a monad A[_]
and a monad B[_]
, then A[B[_]]
can not be derived automatically. However in most cases this can be achieved by having a so-called monad transformer for a given monad.
If we have monad transformer BT
for monad B
, then we can compose a new monad A[B[_]]
for any monad A
. That's right, by using BT
, we can put the B
inside A
.
Monad transformer usage in scalaz
The following assumes scalaz 7, since frankly I didn't use monad transformers with scalaz 6.
A monad transformer MT
takes two type parameters, the first is the wrapper (outside) monad, the second is the actual data type at the bottom of the monad stack. Note: It may take more type parameters, but those are not related to the transformer-ness, but rather specific for that given monad (like the logged type of a Writer
, or the error type of a Validation
).
So if we have a List[Option[A]]
which we would like to treat as a single composed monad, then we need OptionT[List, A]
. If we have Option[List[A]]
, we need ListT[Option, A]
.
How to get there? If we have the non-transformer value, we can usually just wrap it with MT.apply
to get the value inside the transformer. To get back from the transformed form to normal, we usually call .run
on the transformed value.
So val a: OptionT[List, Int] = OptionT[List, Int](List(some(1))
and val b: List[Option[Int]] = a.run
are the same data, just the representation is different.
It was suggested by Tony Morris that is best to go into the transformed version as early as possible and use that as long as possible.
Note: Composing multiple monads using transformers yields a transformer stack with types just the opposite order as the normal data type. So a normal List[Option[Validation[E, A]]]
would look something like type ListOptionValidation[+E, +A] = ValidationT[({type l[+a] = OptionT[List, a]})#l, E, A]
Update: As of scalaz 7.0.0-M2, Validation
is (correctly) not a Monad and so ValidationT
doesn't exist. Use EitherT
instead.
Using WriterT for logging
Based on your need, you can use the WriterT
without any particular outer monad (in this case in the background it will use the Id
monad which doesn't do anything), or can put the logging inside a monad, or put a monad inside the logging.
First case, simple logging
import scalaz.{Writer}
import scalaz.std.list.listMonoid
import scalaz._
def calc1 = Writer(List("doing calc"), 11)
def calc2 = Writer(List("doing other"), 22)
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run should be_== (List("doing calc", "doing other"), 33)
We import the listMonoid
instance, since it also provides the Semigroup[List]
instance. It is needed since WriterT
needs the log type to be a semigroup in order to be able to combine the log values.
Second case, logging inside a monad
Here we chose the Option
monad for simplicity.
import scalaz.{Writer, WriterT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._
def calc1 = WriterT((List("doing calc") -> 11).point[Option])
def calc2 = WriterT((List("doing other") -> 22).point[Option])
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run should be_== (Some(List("doing calc", "doing other"), 33))
With this approach, since the logging is inside the Option
monad, if any of the bound options is None
, we would just get a None
result without any logs.
Note: x.point[Option]
is the same in effect as Some(x)
, but may help to generalize the code better. Not lethal just did it that way for now.
Third option, logging outside of a monad
import scalaz.{Writer, OptionT}
import scalaz.std.list.listMonoid
import scalaz.std.option.optionInstance
import scalaz.syntax.pointed._
type Logger[+A] = WriterT[scalaz.Id.Id, List[String], A]
def calc1 = OptionT[Logger, Int](Writer(List("doing calc"), Some(11): Option[Int]))
def calc2 = OptionT[Logger, Int](Writer(List("doing other"), None: Option[Int]))
val r = for {
a <- calc1
b <- calc2
} yield {
a + b
}
r.run.run should be_== (List("doing calc", "doing other") -> None)
Here we use OptionT
to put the Option
monad inside the Writer
. One of the calculations is None
to show that even in this case logs are preserved.
Final remarks
In these examples List[String]
was used as the log type. However using String
is hardly ever the best way, just some convention forced on us by logging frameworks. It would be better to define a custom log ADT for example, and if needed to output, convert it to string as late as possible. This way you could serialize the log's ADT and easily analyse it later programmatically (instead of parsing strings).
WriterT
has a host of useful methods to work with to ease logging, check out the source. For example given a w: WriterT[...]
, you may add a new log entry using w :++> List("other event")
, or even log using the currently held value using w :++>> ((v) => List("the result is " + v))
, etc.
There are many explicit and longish code (types, calls) in the examples. As always, these are for clarity, refactor them in your code by extracting common types and ops.