Let's define a Kleisli
on \/
:
abstract class MyError
case class NumericalError(msg: String) extends MyError
// Either is a Monad with two type parameters: M[A,B] which represent left and right respectively
// Let's create an ad-hoc type
type EEither[+T] = \/[MyError, T]
and one ad-hoc function for testing purposes:
def safeSqrtEither(t: Double): EEither[Double] =
safeSqrtOpt(t) match {
case Some(r) => r.right
case None => NumericalError("Sqrt on double is not define if _ < 0").left
}
val kSafeSqrtEither = Kleisli.kleisli( (x: Double) => safeSqrtEither(x) )
Function composition works smoothly:
val pipeEither = kSafeSqrtEither >>> kSafeSqrtEither
val r5b = pipeEither2 run 16.0
//which gives r5b: EEither[Double] = \/-(2.0)
I'd like to add logging:
type LoggedROCFun[I,O] = I => WriterT[EEither,scalaz.NonEmptyList[String],O]
val sqrtWithLog: LoggedROCFun[Double, Double] =
(t: Double) =>
WriterT.put(kSafeSqrtEither(t))(s"squared $t".wrapNel)
which seems having the desired behaviour:
val resA = sqrtWithLog(16.0)
// resA: scalaz.WriterT[EEither,scalaz.NonEmptyList[String],Double] = WriterT(\/-((NonEmpty[squared 16.0],4.0)))
Sleek. However, I am struggling to put together an operator which:
WriterT
applying >>>
Desired output:
val combinedFunction = sqrtWithLog >>> sqrtWithLog
val r = combinedFunction run 16.0
// r: WriterT(\/-((NonEmpty[squared 16.0, squared 4.0],2.0)))
My best shot:
def myCompositionOp[I,A,B](f1: LoggedROCFun[I,A])(f2: LoggedROCFun[A,B]): LoggedROCFun[I,B] =
(x: I) => {
val e = f1.apply(x)
val v1: EEither[A] = e.value
v1 match {
case Right(v) => f2(v)
case Left(err) =>
val lastLog = e.written
val v2 = err.left[B]
WriterT.put(v2)(lastLog)
}
}
In the above I first apply f1
to x
, and then I pass along the result to f2
. Otherwise, I short-circuit to Left
.
This is wrong, because in the case Right
I am dropping the previous logging history.
One last Q
val safeDivWithLog: Kleisli[W, (Double,Double), Double] =
Kleisli.kleisli[W, (Double, Double), Double]( (t: (Double, Double)) => {
val (n,d) = t
WriterT.put(safeDivEither(t))(s"divided $n by $d".wrapNel)
}
)
val combinedFunction2 = safeDivWithLog >>> sqrtWithLog
val rAgain = combinedFunction2 run (-10.0,2.0)
// rAgain: W[Double] = WriterT(-\/(NumericalError(Sqrt on double is not define if _ < 0)))
Not sure why the logs are not carried through after a pipeline switches to Left
. Is it because:
therefore I have flipped the order?
Sources: here, scalaz, here, and real world haskell on transformers
You're very close—the issue is just that you've buried your Kleisli
, while you want it on the outside. Your LoggedROCFun
is just an ordinary function, and the Compose
instance for ordinary functions demands that the output of the first function match the type of the input of the second. If you make sqrtWithLog
a kleisli arrow it'll work just fine:
import scalaz._, Scalaz._
abstract class MyError
case class NumericalError(msg: String) extends MyError
type EEither[T] = \/[MyError, T]
def safeSqrtEither(t: Double): EEither[Double] =
if (t >= 0) math.sqrt(t).right else NumericalError(
"Sqrt on double is not define if _ < 0"
).left
type W[A] = WriterT[EEither, NonEmptyList[String], A]
val sqrtWithLog: Kleisli[W, Double, Double] =
Kleisli.kleisli[W, Double, Double](t =>
WriterT.put(safeSqrtEither(t))(s"squared $t".wrapNel)
)
val combinedFunction = sqrtWithLog >>> sqrtWithLog
val r = combinedFunction run 16.0
Note that I've modified your code slightly for the sake of making it a complete working example.
In response to your comment: if you want the writer to accumulate across failures, you'll need to flip the order of Either
and Writer
in the transformer:
import scalaz._, Scalaz._
abstract class MyError
case class NumericalError(msg: String) extends MyError
type EEither[T] = \/[MyError, T]
def safeSqrtEither(t: Double): EEither[Double] =
if (t >= 0) math.sqrt(t).right else NumericalError(
"Sqrt on double is not define if _ < 0"
).left
type W[A] = Writer[List[String], A]
type E[A] = EitherT[W, MyError, A]
val sqrtWithLog: Kleisli[E, Double, Double] =
Kleisli.kleisli[E, Double, Double](t =>
EitherT[W, MyError, Double](safeSqrtEither(t).set(List(s"squared $t")))
)
val constNegative1: Kleisli[E, Double, Double] =
Kleisli.kleisli[E, Double, Double](_ => -1.0.point[E])
val combinedFunction = sqrtWithLog >>> constNegative1 >>> sqrtWithLog
And then:
scala> combinedFunction.run(16.0).run.written
res9: scalaz.Id.Id[List[String]] = List(squared 16.0, squared -1.0)
Note that this won't work with NonEmptyList
in the writer, since you need to be able to return an empty log in the case of e.g. constNegative1.run(0.0).run.written
. I've used a List
, but in real code you'd want a type with less expensive appends.