Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scalaz : validating in a for-comprehension and logging

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

like image 737
bhericher Avatar asked Mar 27 '12 19:03

bhericher


1 Answers

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)
}
like image 170
Yo Eight Avatar answered Apr 02 '23 03:04

Yo Eight