Validation versus disjunction

后端 未结 2 621
深忆病人
深忆病人 2020-12-02 14:51

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

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


        
相关标签:
2条回答
  • 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)))
      )
    
    0 讨论(0)
  • 2020-12-02 15:35

    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.

    0 讨论(0)
提交回复
热议问题