2
votes

This is a follow up to my previous question: Sequencing both Scalaz WriterT and Either with for-yield

The following code block is an example of sequencing Future, Either and Writer using the EitherT and WriterT monad transformers; the following question is about how to subtly change the behaviour of that stack of transformers.

import scalaz._, Scalaz._

class Example[F[_], L] (val logFn: (String) => L)(implicit val f: Monad[F], l: Monoid[L])
{
  type T = Throwable
  type EF[α] = EitherT[F, T, α]
  type WEF[α] = WriterT[EF, L, α]

  private def unreliableInt (i: Int): T Either Int = new java.util.Random ().nextBoolean match {
    case false => Right (i)
    case true => Left (new Exception (":-("))
  }

  private def fn (i: Int): WEF[Int] = WriterT.put[EF, L, Int](EitherT.fromEither[F, T, Int](f.point (unreliableInt (i))))(l.zero)

  private def log (msg: String): WEF[Unit] = WriterT.put[EF, L, Unit](EitherT.right[F, T, Unit](f.point (())))(logFn (msg))

  private def foo (): WEF[Int] = for {
    _ <- log ("Start")
    x <- fn (18)
    _ <- log ("Middle")
    y <- fn (42)
    _ <- log ("End")
  } yield x + y

  def bar (): F[(Option[Int], L)] = {
    val barWEF: WEF[Int] = foo ()

    // Pull out the logs.
    val logsEF: EF[L] = barWEF.written
    val logsF: F[L] = logsEF.toEither.map {
      case Right (x) => x
      case Left (e) => logFn(s"Not the logs we are looking for ${e.getMessage}")
    }

    // Pull out the value.
    val resEF: EF[Int] = barWEF.value
    val resF: F[Option[Int]] = resEF.run.map {
      case \/- (r) => r.some
      case -\/ (ex) => None
    }

    for {
      logs <- logsF
      response <- resF
    } yield (response, logs)
  }
}

object Program
{
  def main (args : Array[String]) = {
    import scala.concurrent._
    import scala.concurrent.duration._
    import ExecutionContext.Implicits.global

    type L = List[String]
    type F[α] = Future[α]

    implicit val l: Monoid[L] = new Monoid[L] { def zero = Nil; def append (f1: L, f2: => L) = f1 ::: f2 }
    implicit val f: Monad[F] = scalaz.std.scalaFuture.futureInstance

    def createLog (s: String) = s :: Nil
    val example = new Example[F, L] (createLog)
    val result = Await.result (example.bar (), 5 seconds)
    println ("Context logs attached:" + result._2.foldLeft ("") { (a, x) => a + "\n$ " + s"$x"})
    println ("Result:" + result._1)
  }
}

The function foo does not behave as I need it to; the function bar and the main function illustrate the problem.

The desired behaviour is such that main will always print one of the following results:

Context logs attached:
$ Start
Result:None

or

Context logs attached:
$ Start
$ Middle
Result:None

or

Context logs attached:
$ Start
$ Middle
$ End
Result:Some(60)

The main function should, however, never print the following:

Context logs attached:
$ Not the logs we are looking for :-(
Result:None

But that is exactly what it does. When both fn1 and fn2 are successful, foo behaves as required and main prints out all of the logs. If either or both fn1 or fn2 return a Left the function bar returns no logs and main goes on to print only the exception. Theres no way to see how far it got in the logs.

It seems that this particular stack of transformers behaves in such a way that if ever there is a -\/ in the sequence, the logging context is simply mapped out...

Looking at the Scalaz code for WriterT this looks likely to be the case:

final case class WriterT[F[_], W, A](run: F[(W, A)])

WriterT is a case class whose only member is run. With respect to this example run is a tuple of our logging context (A) and our result, both wrapped in the same EitherT (F). W and A are bound in data by a type so either they most both be inside a Left or both be inside a Right.

I can speculate that I need a customised version of WriterT that behaves slightly differently, storing its data a little like this, allowing access to the writer part only inside a fresh Applicative[F].point:

final case class WriterT[F[_], W, A](wF: F[W], vF:F[A]) {
  def run: F[(W, A)] = for {
    w <- wF
    v <- vF
  } yield (w, v)
}

Though I'm not really sure if creating my own WriterT type class would be the advisable approach to solving this problem and achieving my desired behaviour.

What are my options?

1

1 Answers

5
votes

This article: Composing monadic effects explains the issue.

So...

type MyMonad e w a = ErrorT e (Writer w) a is isomorphic to (Either e a, w)

type MyMonad e w a = WriterT w (Either e) a is isomorphic to Either r (a, w)

Reordering the stack of monad transformers as follows solves the problem:

import scalaz._, Scalaz._

class Example[F[_], L] (val logFn: (String) => L)(implicit val f: Monad[F], l: Monoid[L])
{
  type T = Throwable
  type WF[α] = WriterT[F, L, α]
  type EWF[α] = EitherT[WF, T, α]

  private def unreliableInt (i: Int): T Either Int = {
    new java.util.Random ().nextBoolean match {
      case false => Right (i)
      case true => Left (new Exception (":-("))
    }
  }

  private def fn (i: Int): EWF[Int] = unreliableInt (i) match {
    case Left (left) => EitherT.left [WF, T, Int] (WriterT.put[F, L, T] (f.point (left))(l.zero))
    case Right (right) => EitherT.right [WF, T, Int] (WriterT.put[F, L, Int] (f.point (right))(l.zero))
  }

  private def log (msg: String): EWF[Unit] = { EitherT.right[WF, T, Unit](WriterT.put[F, L, Unit] (f.point (()))(logFn (msg))) }

  private def foo (): EWF[Int] = for {
    a <- log ("Start")
    x <- fn (18)
    b <- log ("Middle")
    y <- fn (42)
    c <- log ("End")
  } yield x + y

  def bar (): F[(Option[Int], L)] = {
    val barEWF: EWF[Int] = foo ()

    // Pull out the logs.
    val logsF: F[L] = barEWF.run.written

    // Pull out the value.
    val resF: F[Option[Int]] = barEWF.run.value.map {
      case \/- (r) => r.some
      case -\/ (ex) => None
    }

    for {
      logs <- logsF
      response <- resF
    } yield (response, logs)
  }
}

object Program
{
  def main (args : Array[String]) = {
    import scala.concurrent._
    import scala.concurrent.duration._
    import ExecutionContext.Implicits.global

    type L = List[String]
    type F[α] = Future[α]

    implicit val l: Monoid[L] = new Monoid[L] { def zero = Nil; def append (f1: L, f2: => L) = f1 ::: f2 }
    implicit val f: Monad[F] = scalaz.std.scalaFuture.futureInstance

    def createLog (s: String) = s :: Nil
    val example = new Example[F, L] (createLog)
    val result = Await.result (example.bar (), 5 seconds)
    println ("Context logs attached:" + result._2.foldLeft ("") { (a, x) => a + "\n$ " + s"$x"})
    println ("Result:" + result._1)
  }
}