/wdj

A Writer Disjunction monad stack using scalaz, with special semantics for logging and error handling

Primary LanguageScalaMIT LicenseMIT

##WDJ

API Docs

A monad stack.

Specifically an EitherT[Writer[Wr, /[Lf, A]]]

A WDJ is produced by the HasWDJ[Wr, Lf] trait, which requires evidence of a Monoid[Wr], a Semigroup[Lf] and a Wr => Lf.

Because of these requirements, the value accumulated in the Writer can be combined with a Left value encountered by the Disjunction, which supports behavior that is highly convenient for logging and error handling in an application.

Let's go through an example.

####Setting up

First we need to create a HasWDJ to get our WDJ type from. This again requires a Monoid for the Writers value and a Semigroup for the Disjunction Left.

Let's start with some imports.

scala> import com.localytics.{HasWDJ, WDJ}
import com.localytics.{HasWDJ, WDJ}

scala> import com.localytics.syntax.wdj._
import com.localytics.syntax.wdj._

scala> import scalaz.Semigroup
import scalaz.Semigroup

scala> import scalaz.std.list._
import scalaz.std.list._

scala> import scalaz.syntax.either._
import scalaz.syntax.either._

Now we define a Log type. This will serve as our Disjunction Left type, but can also be extracted from a Right using the wdj.logged() method, by way of the Wr => Lf function we must provide.

scala> case class Log(msg: String, trace: List[String])
defined class Log

We need a Semigroup for Log to use it as the Disjunction Left.

scala> val lappend: (Log, => Log) => Log = (l1, l2) => Log(l1.msg, l1.trace ++ l2.trace)
lappend: (Log, => Log) => Log = <function2>

scala> implicit val esg: Semigroup[Log] = Semigroup.instance(lappend)
esg: scalaz.Semigroup[Log] = scalaz.Semigroup$$anon$8@1ab5fae

We can now create a HasWDJ[List[String], Log] by providing a function from List[String] => Log, and get our WDJ from it.

scala> implicit val hasWdjLSE = WDJ((ls: List[String]) => Log("success", ls))
hasWdjLSE: com.localytics.HasWDJ[List[String],Log] = com.localytics.WDJ$$anon$1@2ea3b48a

scala> type WDJ[A] = HasWDJ[List[String], Log]#WDJ[A]
defined type alias WDJ

####Basic Use

With a HasWDJ in scope, we have access to syntax enhancements for working with the WDJ type.

Lift a known successful value of type A into a WDJ[A]

scala> 1.right[Log].toWdj[List[String]]
res0: hasWdjLSE.WDJ[Int] = EitherT(WriterT((List(),\/-(1))))

Lift a known failure into a WDJ[Int]

scala> Log("fubar", List()).left[Int].toWdj[List[String]]
res1: hasWdjLSE.WDJ[Int] = EitherT(WriterT((List(),-\/(Log(fubar,List())))))

Take two operations that may fail, appending to writer of the WDJ and sum their successful values

scala> val x = for {
     |   a <- 1.right[Log].toWdj[List[String]].log(List("got 1"))
     |   b <- 2.right[Log].toWdj[List[String]].log(List("got 2"))
     | } yield a + b
x: scalaz.EitherT[[A]scalaz.WriterT[[+X]X,List[String],A],Log,Int] = EitherT(WriterT((List(got 1, got 2),\/-(3))))

Extract the logged content (Writer contents merged with Left)

scala> x.logged()
res2: Log = Log(success,List(got 1, got 2))

Take two operations that may fail, appending to writer of the WDJ and sum their successful values

Note one operation fails here

scala> val y = for {
     |   a <- 1.right[Log].toWdj[List[String]].log(List("got 1"))
     |   b <- Log("failed", List()).left[Int].toWdj[List[String]].log(List("tried 2"))
     | } yield a + b
y: scalaz.EitherT[[A]scalaz.WriterT[[+X]X,List[String],A],Log,Int] = EitherT(WriterT((List(got 1, tried 2),-\/(Log(failed,List())))))

Extract the logged content (Writer contents merged with Left)

scala> y.logged()
res3: Log = Log(failed,List(got 1, tried 2))

####Bonus Features

Time an operation inside a WDJ recording the elapsed period in the Writer using the given Double => Wr function

scala> 1.right[Log].toWdj[List[String]].mapTimed(d => List("millis-to-add-complete-"++d.toString))(_ + 3)
res4: scalaz.EitherT[[A]scalaz.WriterT[[+X]X,List[String],A],Log,Int] = EitherT(WriterT((List(millis-to-add-complete-1.0),\/-(4))))

Turn a List[WDJ[A]] into a tuple of (List[Lf], List[A]) Useful for accumulating failures and extracting successes

scala> List(1.right[Log].toWdj[List[String]], Log("foo", List()).left[Int].toWdj[List[String]]).tuple()
res5: (List[Log], List[Int]) = (List(Log(foo,List())),List(1))

What's the point?

/ for control flow with success failure semantics

Writer for accumulation of log data

Writer means you can keep all your code side-effect free (at least as far as monitoring/logging) and just deal with it all in one place at the end of your code flow.

And having sugar like the mapTimed/flatMapTimed is cool too.

  • Use WDJ
  • ?
  • Profit