【问题标题】:Validation versus disjunction验证与析取
【发布时间】:2013-12-02 16:04:18
【问题描述】:

假设我想写一个签名如下的方法:

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, _] 没有单子实例)。 p>

在我的第一个实现中,我始终使用ValidationNel,最后使用fold 作为一种假flatMap。第二,我在ValidationNel\/ 之间来回切换,这取决于我是需要错误累积还是单子绑定。它们产生相同的结果。

我在实际代码中使用了这两种方法,但还没有形成对其中一种的偏好。我错过了什么吗? 应该我更喜欢其中一个吗?

【问题讨论】:

  • ValidationNel 上的 flatMap 不会与第一个示例中的 fold 具有相同的结果吗?我知道验证不是 Monad,但它仍然有 flatMap。编辑:刚刚注意到 flatMap 在 7.1 中已被弃用,并附有使用 \\/ 的说明。看来还有第三种选择!我的回答是我只使用\/,因为我不想累积失败。在极少数情况下我会使用ValidationNel
  • flatMap on Validation 已弃用。
  • @drstevens:我经常想积累失败!当你例如处理包含数千个您不想一一识别错误的大型 JSON 文件的目录。
  • 当然会在遍历input: List[(String, String)]时积累错误。不过,您不会通过checkParsescheckValues 累积错误。此外,checkValues 永远不会返回 Nel(InvalidSizes, InvalidSizes, ...),但类型签名表明它可以。我个人永远不会这样做,就像我永远不会 List[a]Option[a] 更准确时一样。我发现这样做会使推理可能的错误范围变得更具挑战性。例如parseFormatB iff parseFormatA 由于 InvalidSizes 而失败。
  • @drstevens:关于checkValues 的返回类型的公平点——这只是一个玩具示例,但我在这里进行了更改。问题的关键是我确实想在checkParses中积累——即如果这对中的任何一个成员都没有解析为整数,我希望看到这两个错误 - 并且在遍历列表时,但不要(不能)在 checkValues 中,它介于两者之间。

标签: validation scala functional-programming scalaz either


【解决方案1】:

这可能不是你要找的答案,但我刚刚注意到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)))
  )

【讨论】:

  • +1,谢谢——我也从来没有关注过@\/,我比我的任何一个都更喜欢这种方法。不过,我会坚持更长时间以获得更大的答案。
【解决方案2】:

以下是我为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
  )

为了更新问题,这段代码是“根据我是否需要错误累积或单子绑定,在ValidatedNelEither 之间来回切换”。

在我提出这个问题后的近六年里,Cats 有 introduced a Parallel type class(在 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

【讨论】:

    猜你喜欢
    • 2012-06-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-11
    相关资源
    最近更新 更多