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