2012-02-24 36 views
14

Me gustaría usar Scalaz para las validaciones y me gusta poder reutilizar las funciones de validación en diferentes contextos. Soy totalmente nuevo en Scalaz por cierto.Componer validaciones de Scalaz

Digamos que tengo estos controles simples:

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] = ... 

Me gustaría ser capaz de componer validaciones, donde se alimenta la salida de uno en el otro. Podría hacerlo fácilmente con flatMap o por medio de comprensión, pero se siente como que debe haber una manera mejor que eso.

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

o

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

// How to combine x1 and x2? 

Cualquier pensamiento de los expertos Scalaz por ahí?

+1

qué pasa "(x1 | @ | x2) {(x1, x2) => ...}" No estoy tan seguro de la sintaxis exacta sin embargo ... Vea http://www.casualmiracles.com/2012/01/16/a-small-example-of-applicative-functors-with-scalaz/ – Jan

Respuesta

13

Es posible que desee echar un vistazo a la Tale of Three Nightclubs que describe la composición de validación usando:

  1. Mónadas (es decir flatMap)
  2. funtores aplicativos dos maneras (usando |@| y traverse)

Básicamente, las reglas equivalen a esto: la composición a través de mónadas es fail-fast. Es decir, su computación se cortocircuitará en este punto y se resolverá en Failure(e). El uso de funtores aplicativos significa que puede acumular fallas (tal vez para la validación de formularios web), lo cual puede hacer usando un collection (que es un Semigroup) como tipo de falla: los ejemplos cancónicos usan NonEmptyList.

Hay otras cosas útiles en Validation así:

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

En el ejemplo específico, ¿por qué le parece que "tiene que haber una manera mejor" que hacerlo a través de una comprensión de? Se puede mejorar un poco, sin embargo:

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

En cuyo caso, que en realidad no merece todo un método:

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) 
+0

En realidad, tengo su esencia en una pestaña abierta ahora mismo. Es un muy buen ejemplo. Lo que estoy luchando es que quiero que mis cheques compongan. Es decir. la salida de una verificación debe ser la entrada a la siguiente. Como en su ejemplo, cuando tiene sus funciones de verificación en una lista, todas ellas controlan la misma instancia de persona que es diferente de lo que estoy tratando de hacer. – chrsan

+0

Usted está describiendo 'flatMap'; es decir, ¡usted ya sabía la respuesta! –

+0

¡Muchas gracias! No, en este caso no necesita un método completo, pero necesito validar lo mismo en diferentes contextos. Es decir. una identificación en sí misma y una entidad que tiene una identificación y algunos otros campos también. Por "una mejor manera" me refería a los métodos cortos y atractivos como los que describes arriba que no son tan obvios para nosotros con un trasfondo imperativo que quiere ser más funcional, etc. – chrsan

17

Además de las soluciones propuestas por @oxbow_lakes, también se puede utilizar Composición de Kleisli.

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] = [email protected] 

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) 

una función del tipo A => M[B] donde M : Monad se llama una flecha Kleisli.

Puede componer dos flechas Kleisli A => M[B] y B => M[C] para conseguir una flecha A => M[C] usando >=> operador. Esto se conoce como composición de Kleisli.

La expresión kleisli(f) >=> kleisli(g) >=> kleisli(h) es equivalente a x => for(a <- f(x); b <- g(a); c <- h(b)) yield c, menos los enlaces locales innecesarios.

+2

¡Oh, para la inferencia del constructor de tipo aplicado parcialmente! –

+0

@oxbow_lakes, esa es una de las cosas más necesarias en Scala. Lamentablemente, no parece estar en su lista a corto plazo. – missingfaktor

+1

como @oxbow_lakes señaló, existe un enfoque de cortocircuito así como un enfoque acumulado. Este ejemplo es el enfoque de cortocircuito. ¿Cómo se haría esto si quisieras acumular los fracasos? – OleTraveler

0

Expresión

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

coulde ser reemplazado de manera tal

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

y el resultado será

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

si ambos validación falla, NonEmptyList contendrá ambos

+1

Er, no, no puede. Se requiere un resultado exitoso para la primera validación como entrada para el segundo, por lo que los funtores aplicativos no pueden ayudarlo aquí. –

0

Recientemente he codificado un simple "marco" para las validaciones declarativas que se pueden componer. Inicialmente he basado mi implementación en la respuesta de @ missingfaktor, sin embargo, además de lo que se le ocurrió, agregué un DSL usando Shapeless's Generic para trabajar con tuplas de tamaño arbitrario de entradas que se validaron y que se incorporan a funciones de coincidencia arity.

Su uso es como sigue:

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

La aplicación se puede encontrar en https://gist.github.com/eallik/eea6b21f8e5154e0c97e.

0

Además de la respuesta de missingfaktor, se puede observar que scalaz 7 no tienen un Monad para Validation debido a la falta de coincidencia de su comportamiento con Apply ejemplo. Así se puede definir Bind para Validation, junto con convertidores para mayor comodidad:

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)