Search code examples
scalascalazmonad-transformerswriter-monad

scalaz - function composition - WriterT


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:

  • combines the values in the WriterT applying >>>
  • chains (appends) each log, keeping track of each step made

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:

  • 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)

therefore I have flipped the order?

Sources: here, scalaz, here, and real world haskell on transformers


Solution

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