2012-02-24 15 views
14

Vorrei utilizzare Scalaz per le convalide e mi piacerebbe poter riutilizzare le funzioni di convalida in diversi contesti. Sono assolutamente nuovo di Scalaz tra i due.Convalida Convalida Scalaz

Diciamo che ho questi semplici controlli:

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

mi piace essere in grado di comporre le convalide in cui l'uscita da una viene immessa nella altro. Potrei facilmente farlo con flatMap o via per le comprensioni, ma sembra che ci debba essere un modo migliore di quello.

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? 

Ogni pensiero da parte degli esperti Scalaz là fuori?

+1

che dire "(x1 | @ | x2) {(x1, x2) => ...}" Io non sono così sicuro di la sintassi esatta però ... Vedi http://www.casualmiracles.com/2012/01/16/a-small-example-of-applicative-functors-with-scalaz/ – Jan

risposta

13

Si potrebbe desiderare di avere uno sguardo alla Tale of Three Nightclubs che descrive la composizione di convalida utilizzando:

  1. Monadi (cioè flatMap)
  2. funtori applicativi in ​​due modi (utilizzando |@| e traverse)

Fondamentalmente le regole ammontano a questo: composizione tramite monadi è fail-fast. Cioè, il tuo calcolo andrà in cortocircuito a questo punto e si risolverà in uno Failure(e). L'uso di funtori applicativi significa che è possibile accumulare guasti (forse per la convalida della forma Web), che si utilizza utilizzando un collection (che è un Semigroup) come tipo di errore: gli esempi di tipo cancerogeno utilizzano NonEmptyList.

c'è altra roba utile su Validation così:

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

Nel tuo esempio specifico, perché pensi che ci "deve essere un modo migliore" che farlo attraverso un per-di comprensione? Può essere migliorato un po ', però:

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

In questo caso, in realtà non merita un intero metodo:

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

In realtà ho il tuo succo in una scheda aperta proprio adesso. È un ottimo esempio La cosa a cui sto combattendo è che voglio che i miei assegni compongano. Cioè l'output da un controllo dovrebbe essere l'input al successivo. Come nel tuo esempio quando hai le tue funzioni di controllo in una lista, fanno tutti il ​​controllo sulla stessa istanza della persona che è diversa da quello che sto cercando di fare. – chrsan

+0

Si sta descrivendo 'flatMap'; Sai già la risposta! –

+0

Grazie mille! No in questo caso non ha bisogno di un intero metodo, ma ho bisogno di convalidare la stessa cosa in diversi contesti. Cioè un id in sé e un'entità che ha un id e anche altri campi. Con "un modo migliore" intendevo metodi brevi dall'aspetto accattivante come quelli che descrivi sopra che non sono così ovvi per noi con uno sfondo imperativo che vuole essere più funzionale ecc. – chrsan

17

In aggiunta alle soluzioni suggerite da @oxbow_lakes, è anche possibile utilizzare Composizione di 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 funzione di tipo A => M[B] dove M : Monad è chiamato una freccia Kleisli.

È possibile comporre due frecce Kleisli A => M[B] e B => M[C] per ottenere una freccia A => M[C] utilizzando >=> operatore. Questo è noto come composizione Kleisli.

L'espressione kleisli(f) >=> kleisli(g) >=> kleisli(h) equivale a x => for(a <- f(x); b <- g(a); c <- h(b)) yield c, meno i binding locali non necessari.

+2

Oh per l'inferenza del costruttore di tipi parzialmente applicata! –

+0

@oxbow_lakes, questa è una delle cose più necessarie in Scala. Purtroppo non sembra essere nella loro lista a breve termine. – missingfaktor

+1

come sottolineato da @oxbow_lakes, vi è un approccio di cortocircuito e un approccio cumulativo. Questo esempio è l'approccio di cortocircuito. Come si farebbe se si volessero accumulare i fallimenti? – OleTraveler

0

Espressione

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

coulde essere sostituito in modo tale

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

e il risultato sarà

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

se entrambi la convalida non riesce, NonEmptyList conterrà entrambi

+1

Ehm, no, non è possibile. Un risultato positivo per la prima convalida è richiesto come input per il secondo, quindi i funtori applicativi non possono aiutarti –

0

Ho recentemente codificato un semplice "framework" per convalide dichiarative che sono componibili. Ho inizialmente basato la mia implementazione sulla risposta di @ missingfaktor, tuttavia, in aggiunta a ciò che ha inventato, ho aggiunto un DSL utilizzando Shapeless's Generic per lavorare con tuple di dimensioni arbitrarie di input da convalidare che vengono inserite nelle funzioni di corrispondenza

Il suo uso è il seguente:

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

L'applicazione può essere trovato alla https://gist.github.com/eallik/eea6b21f8e5154e0c97e.

0

Oltre alla risposta di missingfaktor, si può notare che scalaz 7 non hanno un Monad per Validation causa della mancata corrispondenza del proprio comportamento con Apply esempio. Così si può definire Bind per Validation, insieme a convertitori per convenienza:

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) 
Problemi correlati