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
}
}
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.
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))
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)
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