Question

Suppose I want to write a method with the following signature:

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

For each pair of strings in the input, it needs to verify that both members can be parsed as integers and that the first is smaller than the second. It then needs to return the integers, accumulating any errors that turn up.

First I'll define an error type:

import scalaz._, Scalaz._

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

Now I can implement my method as follows:

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

Or, alternatively:

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
  )

Now for whatever reason the first operation (validating that the pairs parse as strings) feels to me like a validation problem, while the second (checking the values) feels like a disjunction problem, and it feels like I need to compose the two monadically (which suggests that I should be using \/, since ValidationNel[Throwable, _] doesn't have a monad instance).

In my first implementation, I use ValidationNel throughout and then fold at the end as a kind of fake flatMap. In the second, I bounce back and forth between ValidationNel and \/ as appropriate depending on whether I need error accumulation or monadic binding. They produce the same results.

I've used both approaches in real code, and haven't yet developed a preference for one over the other. Am I missing something? Should I prefer one over the other?

Was it helpful?

Solution

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

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top