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
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)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With