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 :
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)
Make the logger a parameter of Validation1, 2 and 3
Wait for the pertinent chapter of "Functional programming in Scala" :)
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