Compose Scalaz validations
Asked Answered
Y

5

14

I would like to use Scalaz for validations and like to be able to reuse the validation functions in different contexts. I'm totally new to Scalaz btw.

Let's say I have these simple checks:

def checkDefined(xs: Option[String]): Validation[String, String] =
  xs.map(_.success).getOrElse("empty".fail)

def nonEmpty(str: String): Validation[String, String] =
  if (str.nonEmpty) str.success else "empty".fail

def int(str: String): Validation[String, Int] = ...

I like to be able to compose validations where output from one is fed into the other. I could easily do that with flatMap or via for comprehensions but it feels like there must be a better way than that.

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- ...
} yield SomeCaseClass(v3, v4)

or

val x1 = checkDefined(map get "foo").flatMap(nonEmpty).flatMap(int)
val x2 = check(...)

// How to combine x1 and x2?

Any thoughts from the Scalaz experts out there?

Yearwood answered 24/2, 2012 at 9:17 Comment(1)
what about "(x1 |@| x2){(x1,x2) => ... }" I'm not so sure about the exact syntax though... See casualmiracles.com/2012/01/16/…Scraggly
T
17

In addition to the solutions suggested by @oxbow_lakes, you can also use Kleisli composition.

scala> import scalaz._, Scalaz._
import scalaz._
import Scalaz._

scala> def f: Int => Validation[String, Int] = i => if(i % 2 == 0) Success(i * 2) else    Failure("Odd!")
f: Int => scalaz.Validation[String,Int]

scala> def g: Int => Validation[String, Int] = i => if(i > 0) Success(i + 1) else Failure("Not positive!")
g: Int => scalaz.Validation[String,Int]

scala> type Va[+A] = Validation[String, A]
defined type alias Va

scala> import Validation.Monad._
import Validation.Monad._

scala> kleisli[Va, Int, Int](f) >=> kleisli[Va, Int, Int](g)
res0: scalaz.Kleisli[Va,Int,Int] = scalaz.Kleislis$$anon$1@4fae3fa6

scala> res0(11)
res1: Va[Int] = Failure(Odd!)

scala> res0(-4)
res2: Va[Int] = Failure(Not positive!)

scala> res0(4)
res3: Va[Int] = Success(9)

A function of type A => M[B] where M : Monad is called a Kleisli arrow.

You can compose two Kleisli arrows A => M[B] and B => M[C] to get an arrow A => M[C] using >=> operator. This is known as Kleisli composition.

The expression kleisli(f) >=> kleisli(g) >=> kleisli(h) is equivalent to x => for(a <- f(x); b <- g(a); c <- h(b)) yield c, minus the unnecessary local bindings.

Ticker answered 24/2, 2012 at 10:21 Comment(7)
Oh for partially-applied type constructor inference!Pannonia
@oxbow_lakes, that's one of the most needed things in Scala. Unfortunately it doesn't appear to be on their short term list.Ticker
as @Pannonia pointed out, there is a short-circuit approach as well as an accumulate approach. This example is the short-circuit approach. How would this be done if you wanted to accumulate the failures?Expeditious
Seriously. Given you can answer the fail-fast case this well, couldn't you read the question?Knapweed
@ms-ati, umm what? This answer is almost a sidenote to the answer at top.Ticker
@Ticker your example does not work anymore for scalaz 7. There is no Validation.Monad object anymore. Any idea how to make kleisli arrow composition work with scalaz7 Validations ?Bush
@Grega, Scalaz has changed a lot since I last used it. I have not been keeping up with times. I am afraid you will have to find the answer for yourself. :-(Ticker
P
14

You might want to have a look at the Tale of Three Nightclubs which describes validation composition using:

  1. Monads (i.e. flatMap)
  2. Applicative functors two ways (using |@| and traverse)

Basically the rules amount to this: composition via monads is fail-fast. That is, your computation will short-circuit at this point and resolve to a Failure(e). Using applicative functors means that you can accumulate failures (maybe for web-form validation) - which you do by using a collection (which is a Semigroup) as the failure type - the canconical examples use NonEmptyList.

There is other useful stuff on Validation as well:

val1 <+> val2    //Acts like an `orElse`
val1 >>*<< val2  //Accumulates both successes and failures

In your specific example, why do you think there "must be a better way" than doing it via a for-comprehension? It can be improved slightly, though:

def checkDefined(xs: Option[String]) = xs.toSuccess("empty :-(")

In which case, it doesn't really deserve a whole method:

for {
  v1 <- map get "foo" toSuccess "Empty :-("
  v2 <- some(v1) filterNot (_.isEmpty) toSuccess "Empty :-("
  v3 <- (v2.parseInt.fail map (_.getMessage)).validation 
  v4 <- ...
} yield SomeCaseClass(v3, v4)
Pannonia answered 24/2, 2012 at 10:1 Comment(6)
Actually I have your gist in a tab open right now. It's a very good example. The thing I'm struggling with is that I want my checks to compose. I.e. the output from one check should be the input to the next. As in your example when you have your check functions in a list they all do the check on the same person instance which is different from what I'm trying to do.Yearwood
You are describing flatMap; i.e. you already knew the answer!Pannonia
Thanks alot! No in this case it doesn't need a whole method, but I need to validate the same thing in different contexts. I.e. an id in itself and an entity that has an id and some other fields as well. By "a better way" I meant cool looking short methods like the ones you describe above that isn't that obvious for us with an imperative background wanting to be more functional etc.Yearwood
Lol, I also have an imperative background. I've been using Validation for about 18 months now and I only "discovered" <+> a few months backPannonia
When I have you on the line... What's the best way to piggyback on an existing validation and just change the failure. Is this the way to go as you do in the sample above checkLong(x).map(Some(_)).fail.map(_ => "Ooops").validationYearwood
I think the other option is to either treat Validation as a Bifunctor and use f <-: vPannonia
C
0

Expression

for {
  v1 <- checkDefined(map.get("foo"))
  v2 <- nonEmpty(v1)
  v3 <- int(v2)
  v4 <- someComputation()
} yield SomeCaseClass(v3, v4)

coulde be replaced in such way

(checkDefined(map.get("foo")).liftFailNel |@| nonEmpty(v1)) {(v1, v2) =
    SomeCaseClass(int(v2), someComputation)
}

and the result will be

 Validtion[NonEmptyList[String], SomeCaseClass] which is equal to ValidationNEL[String, SomeCaseClass]

if both validation fails, NonEmptyList will contain both of them

Caterer answered 24/2, 2012 at 10:55 Comment(1)
Er, no it can't. A successful outcome for the first validation is required as input to the second, so applicative functors cannot help you herePannonia
L
0

I've recently coded a simple "framework" for declarative validations that are composable. I've initially based my implementation on @missingfaktor's answer, however, on top of what he's come up with, I've added a DSL using Shapeless's Generic for working with tuples of arbitrary size of inputs to be validated that are fed in to functions of matching arity.

Its usage is as follows:

def nonEmpty[A] = (msg: String) => Vali { a: Option[A] =>
  a.toSuccess(msg)
}

def validIso2CountryCode = (msg: String) => Vali { x: String =>
  IsoCountryCodes2to3.get(x).toSuccess(msg)
}

val postal = "12345".some
val country = "GB".some

val params = (
  postal
     |> nonEmpty[String]("postal required"),
  country
     |> nonEmpty[String]("country required")
    >=> validIso2CountryCode("country must be valid")
)

// parameter type inference doesn't work here due to the generic type level nature of the implementation; any improvements are welcome!
validate(params) { (postal: String, country: String) =>
  println(s"postal: $postal, country: $country")
}

The implementation can be found at https://gist.github.com/eallik/eea6b21f8e5154e0c97e.

Lidalidah answered 24/10, 2014 at 15:32 Comment(0)
T
0

In addition to missingfaktor's answer, one may note that scalaz 7 don't have a Monad for Validation due to mismatch of its behavior with Apply instance. So one may define Bind for Validation, along with converters for convenience:

import scalaz.{Bind, Kleisli, Validation, Success, Failure}

implicit def toKleisli[E, A, B](f: A => Validation[E, B]): Kleisli[Validation[E, ?], A, B] =
  Kleisli[Validation[E, ?], A, B](f)

implicit def fromKleisli[E, A, B](f: Kleisli[Validation[E, ?], A, B]): A => Validation[E, B] = f.run

implicit def validationBind[E] = new Bind[Validation[E, ?]] {

  def bind[A, B](fa: Validation[E, A])(f: (A) => Validation[E, B]): Validation[E, B] = {
    import Validation.FlatMap._
    fa.flatMap(f)
  }

  def map[A, B](fa: Validation[E, A])(f: (A) => B): Validation[E, B] = fa.map(f)
}

val parse: Option[String] => Validation[String, Int] = checkDefined _ >=> nonEmpty _ >=> int _

println(parse(None)) // Failure(empty)
println(parse(Some(""))) // Failure(empty)
println(parse(Some("abc"))) // Failure(java.lang.NumberFormatException: For input string: "abc")
println(parse(Some("42"))) // Success(42)
Toein answered 28/4, 2017 at 15:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.