验证与析取

19

假设我想编写一个具有以下签名的方法:

def parse(input: List[(String, String)]):
  ValidationNel[Throwable, List[(Int, Int)]]
对于输入中的每对字符串,需要验证两个元素是否都可以解析为整数,并且第一个元素小于第二个元素。然后需要返回这两个整数,累积任何出现的错误。
首先,我将定义一个错误类型:
import scalaz._, Scalaz._

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

现在我可以按照以下方式实施我的方法:

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

或者,另一种选择:

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
  )

现在,无论出于什么原因,第一个操作(验证对是否解析为字符串)感觉像是一个验证问题,而第二个操作(检查值)感觉像是一个联合问题,我需要以单调递增的方式组合这两个操作(这表明我应该使用\/,因为ValidationNel[Throwable, _]没有单调递增实例)。

在我的第一个实现中,我始终使用ValidationNel,然后在最后使用fold作为一种伪flatMap。在第二个实现中,根据需要在ValidationNel\/之间反复跳转,具体取决于我是否需要错误累积或单调绑定。它们产生相同的结果。

我曾在实际代码中使用过这两种方法,但还没有发展出偏好。我错过了什么吗?我应该偏好其中一种吗?


使用ValidationNel的flatMap和第一个示例中的fold不会产生相同的结果吗?我知道Validation不是Monad,但它仍然具有flatMap。编辑:刚注意到在7.1中已弃用flatMap,并提供了使用\\/的说明。看起来会有第三个选项!我的答案是我只使用\/,因为我不想累积失败。我只在极少数情况下使用ValidationNel。 - drstevens
1
@drstevens:我经常希望能够累积失败!例如,当您处理包含数千个大型JSON文件的目录时,您不想一个一个地识别错误。 - Travis Brown
1
当遍历 input: List[(String, String)] 时,我当然会积累错误。但是你在 checkParsescheckValues 中没有积累错误。此外,checkValues 永远不会返回 Nel(InvalidSizes, InvalidSizes, ...),但类型签名表明它可能会这样做。就像我永远不会使用 List[a] 而更准确的是使用 Option[a] 一样,我个人永远不会这样做。我发现这样做使得推断可能出现的错误范围更具挑战性。例如,当 parseFormatA 因为 InvalidSizes 失败时,parseFormatB 才会执行。 - drstevens
1
@drstevens:关于checkValues的返回类型的观点很公正——这只是一个玩具示例,但我已经在这里进行了更改。问题的关键点是我确实想在checkParses中累积——即如果一对中的成员都不能解析为整数,则我想看到两个错误——并且在遍历列表时,但在checkValues中不行(也不能),因为它位于其中间。 - Travis Brown
在这个问题的范围内,“disjunction”是什么意思? - Matt Fenwick
显示剩余4条评论
2个回答

10

这可能不是你要找的答案,但我刚刚注意到Validation有以下方法。


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

当我看到它们时,我并没有真正看出它们的用处,直到我想起了这个问题。它们让你可以通过转换为分离范式来完成适当的绑定。

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

2
+1,谢谢 - 我之前也从来没有注意到 @\/,我觉得这种方法比我的任何一种方法都好。不过,我还会再等一段时间,看看是否有更全面的答案。 - Travis Brown

2
以下是我关于的第二个版本代码的相当准确的翻译:
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
  )

更新问题,这段代码根据需要在ValidatedNelEither之间来回切换,以实现错误累积或单子绑定。
自从我提出这个问题以来,已经有近六年时间了。Cats引入了一个Parallel类型类(在Cats 2.0.0中得到改进),正好解决了我遇到的问题:
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)
  )

我们可以在想要累积错误时切换到应用型运算符的par版本,例如traversetupled,但是否则我们正在使用Either,这给了我们单调绑定,并且我们不再需要引用Validated

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接