Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Chaining Scalaz validation functions: Function1[A,Validation[E,B]]

Tags:

scala

scalaz

I'm trying to write some code to make it easy to chain functions that return Scalaz Validation types. One method I am trying to write is analogous to Validation.flatMap (Short circuit that validation) which I will call andPipe. The other is analogous to |@| on ApplicativeBuilder (accumulating errors) except it only returns the final Success type, which I will call andPass

Suppose I have functions:

def allDigits: (String) => ValidationNEL[String, String]
def maxSizeOfTen: (String) => ValidationNEL[String, String] 
def toInt: (String) => ValidationNEL[String, Int]

As an example, I would like to first pass the input String to both allDigits and maxSizeOf10. If there are failures, it should short circuit by not calling the toInt function and return either or both failures that occurred. If Successful, I would like to pass the Success value to the toInt function. From there, it would either Succeed with the output value being an Int, or it would fail returning only the validation failure from toInt.

def intInput: (String) => ValidationNEL[String,Int] = (allDigits andPass maxSizeOfTen) andPipe toInt 

Is there a way to do this without my add-on implementation below?

Here is my Implementation:

  trait ValidationFuncPimp[E,A,B] {
    val f: (A) => Validation[E, B]

    /** If this validation passes, pass to f2, otherwise fail without accumulating. */
    def andPipe[C](f2: (B) => Validation[E,C]): (A) => Validation[E,C] = (a: A) => {
      f(a) match {
        case Success(x) => f2(x)
        case Failure(x) => Failure(x)
      }
    }

    /** Run this validation and the other validation, Success only if both are successful.  Fail accumulating errors. */
    def andPass[D](f2: (A) => Validation[E,D])(implicit S: Semigroup[E]): (A) => Validation[E,D] = (a:A) => {
      (f(a), f2(a)) match {
        case (Success(x), Success(y)) => Success(y)
        case (Failure(x), Success(y)) => Failure(x)
        case (Success(x), Failure(y)) => Failure(y)
        case (Failure(x), Failure(y)) => Failure(S.append(x, y))
      }
    }
  }
  implicit def toValidationFuncPimp[E,A,B](valFunc : (A) => Validation[E,B]): ValidationFuncPimp[E,A,B] = {
    new ValidationFuncPimp[E,A,B] {
      val f = valFunc
    }
  }
like image 653
Travis Stevens Avatar asked Dec 19 '12 21:12

Travis Stevens


3 Answers

I'm not claiming that this answer is necessarily any better than drstevens's, but it takes a slightly different approach and wouldn't fit in a comment there.

First for our validation methods (note that I've changed the type of toInt a bit, for reasons I'll explain below):

import scalaz._, Scalaz._

def allDigits: (String) => ValidationNEL[String, String] =
  s => if (s.forall(_.isDigit)) s.successNel else "Not all digits".failNel

def maxSizeOfTen: (String) => ValidationNEL[String, String] =
  s => if (s.size <= 10) s.successNel else "Too big".failNel

def toInt(s: String) = try(s.toInt.right) catch {
  case _: NumberFormatException => NonEmptyList("Still not an integer").left
}

I'll define a type alias for the sake of convenience:

type ErrorsOr[+A] = NonEmptyList[String] \/ A

Now we've just got a couple of Kleisli arrows:

val validator = Kleisli[ErrorsOr, String, String](
  allDigits.flatMap(x => maxSizeOfTen.map(x *> _)) andThen (_.disjunction)
)

val integerizer = Kleisli[ErrorsOr, String, Int](toInt)

Which we can compose:

val together = validator >>> integerizer

And use like this:

scala> together("aaa")
res0: ErrorsOr[Int] = -\/(NonEmptyList(Not all digits))

scala> together("12345678900")
res1: ErrorsOr[Int] = -\/(NonEmptyList(Too big))

scala> together("12345678900a")
res2: ErrorsOr[Int] = -\/(NonEmptyList(Not all digits, Too big))

scala> together("123456789")
res3: ErrorsOr[Int] = \/-(123456789)

Using flatMap on something that isn't monadic makes me a little uncomfortable, and combining our two ValidationNEL methods into a Kleisli arrow in the \/ monad—which also serves as an appropriate model for our string-to-integer conversion—feels a little cleaner to me.

like image 137
Travis Brown Avatar answered Nov 16 '22 20:11

Travis Brown


This is relatively concise with little "added code". It is still sort of wonky though because it ignores the successful result of applying allDigits.

scala> val validated = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x *> y
validated: String => scalaz.Validation[scalaz.NonEmptyList[String],String] = <function1>

scala> val validatedToInt = (str: String) => validated(str) flatMap(toInt)
validatedToInt: String => scalaz.Validation[scalaz.NonEmptyList[String],Int] = <function1>

scala> validatedToInt("10")
res25: scalaz.Validation[scalaz.NonEmptyList[String],Int] = Success(10)

Alternatively you could keep both of the outputs of allDigits and maxSizeOfTen.

val validated2 = for {
  x <- allDigits
  y <- maxSizeOfTen
} yield x <|*|> y

I'm curious if someone else could come up with a better way to combine these. It's not really composition...

val validatedToInt = (str: String) => validated2(str) flatMap(_ => toInt(str))

Both validated and validated2 accumulate failures as shown below:

scala> def allDigits: (String) => ValidationNEL[String, String] = _ => failure(NonEmptyList("All Digits Fail"))
allDigits: String => scalaz.Scalaz.ValidationNEL[String,String]

scala> def maxSizeOfTen: (String) => ValidationNEL[String, String] = _ => failure(NonEmptyList("max > 10"))
maxSizeOfTen: String => scalaz.Scalaz.ValidationNEL[String,String]

scala> val validated = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x *> y
validated: String => scalaz.Validation[scalaz.NonEmptyList[String],String] = <function1>

scala> val validated2 = for {
     |   x <- allDigits
     |   y <- maxSizeOfTen
     | } yield x <|*|> y
validated2: String => scalaz.Validation[scalaz.NonEmptyList[String],(String, String)] = <function1>

scala> validated("ten")
res1: scalaz.Validation[scalaz.NonEmptyList[String],String] = Failure(NonEmptyList(All Digits Fail, max > 10))

scala> validated2("ten")
res3: scalaz.Validation[scalaz.NonEmptyList[String],(String, String)] = Failure(NonEmptyList(All Digits Fail, max > 10))
like image 42
drstevens Avatar answered Nov 16 '22 22:11

drstevens


Use ApplicativeBuilder with the first two, so that the errors accumulate, then flatMap toInt, so toInt only gets called if the first two succeed.

val validInt: String => ValidationNEL[String, Int] = 
  for {
    validStr <- (allDigits |@| maxSizeOfTen)((x,_) => x); 
    i <- toInt
  } yield(i)
like image 1
stew Avatar answered Nov 16 '22 22:11

stew