Validation versus disjunction
The following is a pretty close translation of the second version of my code for Cats:
import scala.util.Try
case class InvalidSizes(x: Int, y: Int) extends Exception(
s"Error: $x is not smaller than $y!"
)
def parseInt(input: String): Either[Throwable, Int] = Try(input.toInt).toEither
def checkValues(p: (Int, Int)): Either[InvalidSizes, (Int, Int)] =
if (p._1 >= p._2) Left(InvalidSizes(p._1, p._2)) else Right(p)
import cats.data.{EitherNel, ValidatedNel}
import cats.instances.either._
import cats.instances.list._
import cats.syntax.apply._
import cats.syntax.either._
import cats.syntax.traverse._
def checkParses(p: (String, String)): EitherNel[Throwable, (Int, Int)] =
(parseInt(p._1).toValidatedNel, parseInt(p._2).toValidatedNel).tupled.toEither
def parse(input: List[(String, String)]): ValidatedNel[Throwable, List[(Int, Int)]] =
input.traverse(fields =>
checkParses(fields).flatMap(s => checkValues(s).toEitherNel).toValidated
)
To update the question, this code is "bouncing back and forth between ValidatedNel
and Either
as appropriate depending on whether I need error accumulation or monadic binding".
In the almost six years since I asked this question, Cats has introduced a Parallel
type class (improved in Cats 2.0.0) that solves exactly the problem I was running into:
import cats.data.EitherNel
import cats.instances.either._
import cats.instances.list._
import cats.instances.parallel._
import cats.syntax.either._
import cats.syntax.parallel._
def checkParses(p: (String, String)): EitherNel[Throwable, (Int, Int)] =
(parseInt(p._1).toEitherNel, parseInt(p._2).toEitherNel).parTupled
def parse(input: List[(String, String)]): EitherNel[Throwable, List[(Int, Int)]] =
input.parTraverse(fields =>
checkParses(fields).flatMap(checkValues(_).toEitherNel)
)
We can switch the the par
version of our applicative operators like traverse
or tupled
when we want to accumulate errors, but otherwise we're working in Either
, which gives us monadic binding, and we no longer have to refer to Validated
at all.
This is probably not the answer you're looking, but I just noticed Validation
has the following methods
/** 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)
When I saw them, I couldn't really see their usefulness until I remembered this question. They allow you to do a proper bind by converting to disjunction.
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).@\/(_.flatMap(checkValues(_).leftMap(_.wrapNel)))
)