问题
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:
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)
}
}
来源:https://stackoverflow.com/questions/31674741/customising-composition-of-future-either-and-writer-in-scalaz