Validazione contro disgiunzione

Supponiamo di voler scrivere un metodo con la seguente firma:

def parse(input: List[(String, String)]): ValidationNel[Throwable, List[(Int, Int)]] 

Per ogni coppia di stringhe nell’input, è necessario verificare che entrambi i membri possano essere analizzati come numeri interi e che il primo sia più piccolo del secondo. Quindi ha bisogno di restituire gli interi, accumulando eventuali errori che si presentano.

Innanzitutto definirò un tipo di errore:

 import scalaz._, Scalaz._ case class InvalidSizes(x: Int, y: Int) extends Exception( s"Error: $x is not smaller than $y!" ) 

Ora posso implementare il mio metodo come segue:

 def checkParses(p: (String, String)): ValidationNel[NumberFormatException, (Int, Int)] = p.bitraverse[ ({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int ]( _.parseInt.toValidationNel, _.parseInt.toValidationNel ) def checkValues(p: (Int, Int)): Validation[InvalidSizes, (Int, Int)] = if (p._1 >= p._2) InvalidSizes(p._1, p._2).failure else p.success def parse(input: List[(String, String)]): ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p => checkParses(p).fold(_.failure, checkValues _ andThen (_.toValidationNel)) ) 

Oppure, in alternativa:

 def checkParses(p: (String, String)): NonEmptyList[NumberFormatException] \/ (Int, Int) = p.bitraverse[ ({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int ]( _.parseInt.toValidationNel, _.parseInt.toValidationNel ).disjunction def checkValues(p: (Int, Int)): InvalidSizes \/ (Int, Int) = (p._1 >= p._2) either InvalidSizes(p._1, p._2) or p def parse(input: List[(String, String)]): ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p => checkParses(p).flatMap(s => checkValues(s).leftMap(_.wrapNel)).validation ) 

Ora per qualsiasi ragione la prima operazione (convalidare che le coppie analizzino come stringhe) mi sembra un problema di validazione, mentre la seconda (controllare i valori) si sente come un problema di disgiunzione, e mi sembra di dover comporre i due monadicamente ( che suggerisce che dovrei usare \/ , poiché ValidationNel[Throwable, _] non ha un’istanza monad).

Nella mia prima implementazione, utilizzo ValidationNel dappertutto e poi piego alla fine come una specie di flatMap finta. Nel secondo, rimbalzo avanti e indietro tra ValidationNel e \/ come appropriato a seconda se ho bisogno di accumulo di errori o vincoli monadici. Producono gli stessi risultati.

Ho usato entrambi gli approcci nel codice reale e non ho ancora sviluppato una preferenza per l’uno rispetto all’altro. Mi sto perdendo qualcosa? Dovrei preferire uno rispetto all’altro?

Questa probabilmente non è la risposta che stai cercando, ma ho appena notato che Validation ha i seguenti metodi

 /** Run a disjunction function and back to validation again. Alias for `@\/` */ def disjunctioned[EE, AA](k: (E \/ A) => (EE \/ AA)): Validation[EE, AA] = k(disjunction).validation /** Run a disjunction function and back to validation again. Alias for `disjunctioned` */ def @\/[EE, AA](k: (E \/ A) => (EE \/ AA)): Validation[EE, AA] = disjunctioned(k) 

Quando li ho visti, non potevo davvero vedere la loro utilità finché non ho ricordato questa domanda. Ti consentono di eseguire un corretto binding convertendo in disgiunzione.

 def checkParses(p: (String, String)): ValidationNel[NumberFormatException, (Int, Int)] = p.bitraverse[ ({ type L[x] = ValidationNel[NumberFormatException, x] })#L, Int, Int ]( _.parseInt.toValidationNel, _.parseInt.toValidationNel ) def checkValues(p: (Int, Int)): InvalidSizes \/ (Int, Int) = (p._1 >= p._2) either InvalidSizes(p._1, p._2) or p def parse(input: List[(String, String)]): ValidationNel[Throwable, List[(Int, Int)]] = input.traverseU(p => checkParses(p)[email protected]\/(_.flatMap(checkValues(_).leftMap(_.wrapNel))) )