Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you use scalaz.WriterT for logging in a for expression?

Tags:

How do you use scalaz.WriterT for logging?

like image 861
Keynan Avatar asked Aug 14 '12 01:08

Keynan


1 Answers

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 Noneto 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.

like image 56
ron Avatar answered Oct 15 '22 11:10

ron