9

I admit that the title is not very explicit : sorry for that.

Assume I have a for-comprehension :

for {v1<-Validation1(input)
     v2<-Validation2(v1)
     v3<-Validation3(v2)
} yield result

Validation1, Validation2 and Validation3 do some checking (e.g "age > 18") and use fail/success ; so if something is wrong, the for-comprehension aborts and I get the reason in the failure part of the result, else I get the expected value in the success part. So far, so good and nothing very difficult.

But Validation1, Validation2, Validation3 are successfull if their input satisfies some rules (e.g : "the guy can vote because his age is greater than 18 and his nationality is French") . what I want is to keep trace of the rules that are applied in order to be able to display them at the end.

It's clearly a use case of logging. but I hesitate on the way to do it :

  1. Have an object "logger" that is accessible by any function (Validation1, 2 and 3 but also the caller that wants to display the content of the log)

  2. Make the logger a parameter of Validation1, 2 and 3

  3. Wait for the pertinent chapter of "Functional programming in Scala" :)

  4. Other?

Thank for your advices

Edited on april 10

So, suppose I want to compute the function : x -> 1/sqrt(x)

First, I compute sqrt(x) by checking that x > 0 and then I take the inverse if not zero.

with scalaz.Validation, it is simple :

val failsquareroot= "Can't take squareroot of negative number"
val successsquareroot= "Squareroot ok"
val failinverse="Can't take inverse of zero"
val successinverse=  "Inverse ok"

def squareroot(x:Double)=if (x < 0) failsquareroot.fail else sqrt(x).success
def inverse(x:Double)= if (x == 0) failinverse.fail else (1/x).success
def resultat(x:Double)= for {
   y <- squareroot(x)
   z<-inverse(y)
} yield z

Now, if squareroot successes, I want to log the string successsquaretoot and if inverse sucesses, I want to log the string successinverse so that the function resultat accumulates the both strings in case of success

I started with ValidationT as Yo Eight suggested :

 def squareroot2(x:Double)=ValidationT[({type f[x] = Writer[String,x]})#f, String,Double](Writer(successsquareroot,squareroot(x)))
 def inverse2(x:Double)=ValidationT[({type f[x] = Writer[String,x]})#f, String,Double](Writer(successinverse,inverse(x)))  

But I can't find how to combine them in a for-comprehension. Furthermore, to get the result of one of them, I have to write : squareroot2(4).run.run which seems strange and in the way I wrote it, even in case of failure the strings successsquareroot is logged :

 println(squareroot2(-1).run.run)

prints : (Squareroot ok,Failure(Can't take squareroot of negative number))

Thank you! Benoit

Edited on april 12

So Yo Eight suggested this snippet :

 def squareroot(x:Double) = if (x < 0) failureT("Can't take squareroot of negative  number") else successT(sqrt(x))

 def inverse(x:Double) = if (x == 0) failureT("Can't take inverse of zero ") else successT(1/x)

 for {
    y <- squareroot(x).flatMapF(i => Writer("Squareroot ok", i))
   z <- inverse(y).flatMapF(i => Writer("Inverse ok", i))
 } yield z

and he warned me that some type annotations was necessary. Effectivly, the return tpye of squareroot and inverse is rather ugly : it's a ValidationT of something that I had difficulties to understand!

So, I had to specify the return type explictly : def inverse(x:Double) : ValidationT[?,E,A] where "E" is String and "A" is Double (that was easy!). But what about the first one? It must be a monad (as far as I understand) and I choosed the simpliest : Id (that is Identity).

So now we have :

   def squareroot(x:Double):ValidationT[Id,String,Double]=if (x < 0)  failureT(failsquareroot) else successT(sqrt(x))
   def inverse(x:Double):ValidationT[Id,String,Double]=if (x == 0) failureT(failinverse)else successT(1/x)     

But the for-comprehension doesn't compile because "y" is not a Double but a WriterT[Id, String, Double] Furthermore, the first logged message ("Squareroot ok") is "lost".

Eventually, I did like that :

   def resultat(x:Double) = for {
       y <- squareroot(x).flatMapF(i => Writer("Squareroot ok", i))
       z <- inverse(y.run._2).flatMapF(i => Writer(y.run._1 + ", Inverse ok", i))
   } yield z.run //Note that writing "z.run.run" doesn't compile

   println("0 : " + resultat(0.0).run)
   println("-1 : " +resultat(-1.0).run)
   println("4 : " + resultat(4).run)

which gives :

  0 : Failure(Can't take inverse of zero)
  -1 : Failure(Can't take squareroot of negative number)
  4 : Success((Squareroot ok, Inverse ok,0.5)

Cool! I would be better to use a List[String] for the Writer, but I think that I'm on the good way!

And now, I can think to my holidays (tomorrow!) :)

Edited on may 14

well, the code doesn't compile, but the error is in Yo Eight's last suggestion (Note that it is not an offense again Yo Eight who is a model of kindness!) . I submit you the full code and the error :

import scala.math._
import scalaz._
import Scalaz._

object validlog extends ValidationTFunctions {



val failsquareroot= "Can't take squareroot of negative number"
val successsquareroot= "Squareroot ok"
val failinverse="Can't take inverse of zero"
val successinverse=  "Inverse ok"

case class MyId[A]( v: A)

implicit val myIdPointed = new Pointed[MyId]{
  def point[A](v: => A) = MyId(v)

}

implicit def unId[A](my: MyId[A]): A = my.v

def squareroot(x:Double):ValidationT[({type f[x] = WriterT[MyId,String, x]})#f,String,Double]=if (x < 0) failureT[({type f[x] = WriterT[MyId,String, x]})#f,String,Double](failsquareroot) else successT[({type f[x] = WriterT[MyId,String, x]})#f,String,Double](sqrt(x))

def inverse(x:Double):ValidationT[({type f[x] = WriterT[MyId, String, x]})#f,String,Double]=if (x == 0) failureT[({type f[x] = WriterT[MyId,String, x]})#f,String,Double](failinverse) else successT[({type f[x] = WriterT[MyId,String, x]})#f,String,Double](1/x)


   /* def resultat(x:Double) = for {
       y <- squareroot(x).flatMapF(i => Writer(", Squareroot ok", i))
       z <- inverse(y).flatMapF(i => Writer(", Inverse ok", i))
   } yield z */

   def main(args: Array[String]): Unit = {
    println(inverse(0.0).run)
    println(inverse(0.5).run)
    println(squareroot(-1.0).run)
    println(inverse(4.0).run)
  }



}

Here is the terminal's session :

benoit@benoit-laptop:~$ cd scala
benoit@benoit-laptop:~/scala$ scala -version
Scala code runner version 2.9.2 -- Copyright 2002-2011, LAMP/EPFL
benoit@benoit-laptop:~/scala$ scala -cp ./scalaz7/scalaz-core_2.9.2-7.0-SNAPSHOT.jar validlog.scala
/home/benoit/scala/validlog.scala:15: error: object creation impossible, since method  map in trait Functor of type [A, B](fa: Main.MyId[A])(f: A => B)Main.MyId[B] is not defined
implicit val myIdPointed = new Pointed[MyId]{
                           ^
    one error found

I guess there is something that I've missed from the beginning that could explain why I'm sticked for some weeks!

Benoit

Edited on may 15

Compiling your code, I have a first error :

 could not find implicit value for parameter F:  scalaz.Pointed[Main.$anon.ValidationTExample.WriterAlias]

After some tries, I rewrote the import in this manner :

import scalaz.Writer
import scalaz.std.string._
import scalaz.Id._
import scalaz.WriterT
import scalaz.ValidationT
import scala.Math._

There is still one error :

 error: could not find implicit value for parameter F: scalaz.Monad[[x]scalaz.WriterT[[+X]X,String,x]]
     y <- squareroot(x).flatMapF(i => Writer("Squareroot ok", i))
                           ^
one error found

This error was present with the code you wrote on may 14. Obviously, it is difficult to understand what to iimport exactly with scalaz-seven. Using the version 6, things looked simpler : one just had to import scalaz._ and Scalaz._

I feel like a "desperate housewriter" :) (yes, I agree, it is not very astute but it's relaxing!)

Benoit

May 23

Ouf! It effectively works with the last version of scalaz-seven : note that I had to build it instead of downloading a snapshot.

that's great!

For those who are interested, here is the output :

 0 : (Squareroot ok,Failure(Can't take inverse of zero ))
-1 : (,Failure(Can't take squareroot of negative number))
 4 : (Squareroot ok, Inverse ok,Success(0.5))

Yo Eight, if by chance we meet one day, i'll pay you a beer!

Benoit

bhericher
  • 667
  • 4
  • 13
  • I think your version of scalaz-seven is not good. The code is running correctly with the last version of scalaz-seven branch – Yo Eight May 16 '12 at 08:07
  • I missed your post. That is what I guessed. I'm using the snapshot of april 14. I should have got time to test with the last version this evening – bhericher May 23 '12 at 14:44

1 Answers1

7

In order to log during monadic computation, you have to use an instance of Writer monad. Since monad doesn't compose and you want to keep "Validation" effect, you should use a Validation Monad Transformer. I don't know which version of ScalaZ you're using but Scalaz7 (branch scalaz-seven) provides such monad transformer (namely ValidationT).

so we get:

ValidationT[({type f[x] = Writer[W, x]})#f, A]

with W the type of your logger

According to your edit, here's how I'll do it

def squareroot(x:Double) = if (x < 0) failureT("Can't take squareroot of negative number") else successT(sqrt(x))

def inverse(x:Double) = if (x == 0) failureT("Can't take inverse of zero ") else successT(1/x)

And now, how to use it in a for-comprehension

for {
  y <- squareroot(x).flatMapF(i => Writer("Squareroot ok", i))
  z <- inverse(y).flatMapF(i => Writer("Inverse ok", i))
} yield z

Those snippets might need more type annotations

Edited on april 13

Here's the correct type annotations for your methods:

 def squareroot(x:Double):ValidationT[({type f[x] = Writer[String, x]})#f,String,Double]
 def inverse(x:Double):ValidationT[{type f[x] = Writer[String, x]})#f,String,Double]

That way, you can define resultat method like this:

def resultat(x:Double) = for {
   y <- squareroot(x).flatMapF(i => Writer(", Squareroot ok", i))
   z <- inverse(y).flatMapF(i => Writer(", Inverse ok", i))
} yield z

You could also use List[String] as a log type because it's a monoid

BTW, I speak French if it can help :-)

Edit on May 14

The problem was: The compiler cannot resolve

implicitly[Pointed[({ type f[x] = Writer[String, x] })#f]]

because WriterT need an instance of Monoid[String] and Pointed[Id].

import std.string._ // this import all string functions and instances
import Id._         // this import all Id functions and instances

Here is the full executable code

import scalaz._
import std.string._
import Id._
import scalaz.WriterT
import scalaz.ValidationT
import scala.Math._

object ValidationTExample extends Application {
  type ValidationTWriterAlias[W, A] = ValidationT[({type f[x] = Writer[W, x]})#f, W, A]
  type WriterAlias[A] = Writer[String, A]

  def squareroot(x:Double): ValidationTWriterAlias[String, Double] = 
    if (x < 0) ValidationT.failureT[WriterAlias, String, Double]("Can't take squareroot of negative number") 
    else ValidationT.successT[WriterAlias, String, Double](sqrt(x))

  def inverse(x:Double): ValidationTWriterAlias[String, Double] = 
    if (x == 0) ValidationT.failureT[WriterAlias, String, Double]("Can't take inverse of zero ") 
    else ValidationT.successT[WriterAlias, String, Double](1/x)

  def resultat(x:Double) = for {
    y <- squareroot(x).flatMapF(i => Writer("Squareroot ok", i))
    z <- inverse(y).flatMapF(i => Writer(", Inverse ok", i))
  } yield z

  println("0 : " + resultat(0.0).run.run)
  println("-1 : " + resultat(-1.0).run.run)
  println("4 : " + resultat(4).run.run)
}

Edit on August 14

This code is no longer valid in scalaz-seven. ValidationT has been removed since Validation is not a monad. Hopefully, EitherT can be used instead. Besides, a new MonadWriter/ListenableMonadWriter typeclass has been added to alleviate those type annotations.

import scalaz._
import std.string._
import syntax.monadwriter._
import scala.Math._

object EitherTExample extends Application {
  implicit val monadWriter = EitherT.monadWriter[Writer, String, String]

  def squareroot(x: Double) =
    if (x < 0)
      monadWriter.left[Double]("Can't take squareroot of negative number")
    else
      monadWriter.right[Double](sqrt(x))

  def inverse(x: Double) = 
    if (x == 0)
      monadWriter.left[Double]("Can't take inverse of zero")
    else
      monadWriter.right[Double](1 / x)

  def resultat(x: Double) = for {
    y <- squareroot(x) :++> "Squareroot ok"
    z <- inverse(y)    :++> ", Inverse ok"
  } yield z

  println("0 : " + resultat(0.0).run.run)
  println("-1 : " + resultat(-1.0).run.run)
  println("4 : " + resultat(4).run.run)
}
Yo Eight
  • 467
  • 2
  • 5
  • Thank you Yo Eight : I use Scalaz 6.0.4 and will check what it provides. Anyway, I understand the idea. – bhericher Mar 28 '12 at 14:17
  • I'm sticked! If I replace Validation1, Validation2 and Validation3 by ValidationT[...](Writer("blabla",Validation1(x)) etc..., I successfully chain the 3 validations, but I don't see how to accumulate the logs... – bhericher Apr 05 '12 at 20:09
  • 1
    The log is "accumulated" implicitly when Writer.flatMap is called (an instance of Monoid[W] is required). Writer.flatMap is called implicitly when ValidationT.flatMap is called. – Yo Eight Apr 09 '12 at 18:17
  • Thank you Yo eight, but I'm still in trouble... I'm going to edit my question in order to show what I'm doing. – bhericher Apr 10 '12 at 19:45
  • Thanks again Yo Eight, but I give up for a while... I think that I have to investigate more deeply in scalaz, monads tranformer, etc... before I feel at ease – bhericher Apr 11 '12 at 20:02
  • Finally, it made me a little me obsessed... and I have reached a point that looks satisfying, but maybe it can be improved (I've updated the question above). Many thanks to Yo Eight for your help! – bhericher Apr 12 '12 at 18:22
  • Ok, I've seen your last edit. I'll rewrite my code in a couple of days... Un grand merci! – bhericher Apr 13 '12 at 18:47
  • I'm still puzzled : failureT and successT are asking for an implicit parameter "Pointed[F]". I guess that F is my Writer, but I can't find any Pointed[Writer[...]] in Scalaz nor can I figure how to write it. Maybe I'm trying to do something out of my skills and I should try simpler monads transformers first? thanks for your advices. – bhericher Apr 25 '12 at 15:34
  • I'll try ASAP. Maybe this week-end or next week. Thanks! – bhericher May 11 '12 at 12:19
  • Thank you for this update with EitherT : I"d effectively noticed on scalaz's mailinglist that Validation is not a monad (I don"t know the reason, but that is another question!). There is a similar thread on stackoverflow that I found interesting : http://stackoverflow.com/questions/11944694/how-do-you-use-scalaz-writert-for-logging-in-a-for-expression – bhericher Aug 22 '12 at 16:40