【问题标题】:How to accumulate errors in Either?如何在Either中累积错误?
【发布时间】:2014-01-25 13:51:05
【问题描述】:

假设我有几个案例类和函数来测试它们:

case class PersonName(...)
case class Address(...)
case class Phone(...)

def testPersonName(pn: PersonName): Either[String, PersonName] = ...
def testAddress(a: Address): Either[String, Address] = ...
def testPhone(p: Phone): Either[String, Phone] = ...

现在我定义了一个新的案例类 Person 和一个测试函数,很快就失败了

case class Person(name: PersonName, address: Address, phone: Phone)

def testPerson(person: Person): Either[String, Person] = for {
  pn <- testPersonName(person.name).right
  a <- testAddress(person.address).right
  p <- testPhone(person.phone).right
} yield person;

现在我希望函数testPerson 累积错误,而不是快速失败。

我希望testPerson 始终执行所有这些test* 函数并返回Either[List[String], Person]。我该怎么做?

【问题讨论】:

  • 我知道这不是你想听到的,但错误累积和for-comprehensions(或任何类型的单子排序)只是不混合。例如,如果您在对testAddress 的调用中使用了pn 并且testPersonName 失败了怎么办?
  • 看看ValidationNel 来自scalaz。见Learning scalaz/Validation。在你的情况下:def tesPersonName(pn: PersonName): ValidationNel[String, PersonName] = ... => (testPersonName(person.name) |@| testAddress(person.address) |@| testPhone(person.phone))(Person).
  • 你也可以看看this library。它允许您以您想要的方式组合EitherRight(Person)(testPersonName(person.name), testAddress(person.address), testPhone(person.phone))。免责声明:我没有尝试过这个库。

标签: scala functional-programming either


【解决方案1】:

Scala 的for-comprehensions(它对flatMapmap 的调用组合进行了脱糖)旨在允许您以这样一种方式对单子计算进行排序,以便您可以访问早期计算的结果后续步骤。考虑以下几点:

def parseInt(s: String) = try Right(s.toInt) catch {
  case _: Throwable => Left("Not an integer!")
}

def checkNonzero(i: Int) = if (i == 0) Left("Zero!") else Right(i)

def inverse(s: String): Either[String, Double] = for {
  i <- parseInt(s).right
  v <- checkNonzero(i).right
} yield 1.0 / v

这不会累积错误,事实上没有合理的方法可以。假设我们调用inverse("foo")。那么parseInt 显然会失败,这意味着我们无法获得i 的值,这意味着我们无法继续执行序列中的checkNonzero(i) 步骤。

在您的情况下,您的计算没有这种依赖性,但是您使用的抽象(单子排序)不知道这一点。你想要的是一个类似Either 的类型,它不是一元的,而是应用。有关差异的一些详细信息,请参阅my answer here

例如,您可以使用ScalazValidation 编写以下内容,而无需更改任何个人验证方法:

import scalaz._, syntax.apply._, syntax.std.either._

def testPerson(person: Person): Either[List[String], Person] = (
  testPersonName(person.name).validation.toValidationNel |@|
  testAddress(person.address).validation.toValidationNel |@|
  testPhone(person.phone).validation.toValidationNel
)(Person).leftMap(_.list).toEither

当然,这比必要的更冗长,并且会丢弃一些信息,并且在整个过程中使用Validation 会更简洁一些。

【讨论】:

  • &lt;console&gt;:31: error: value validation is not a member of Either[String,Int] 我应该导入什么? myEither.disjunction.validation.toValidationNel 工作正常。
  • 哦,对了——.validation 上的Eithernew in 7.1。所以,是的,通过析取或使用Validation.fromEither 是您在 7.0 中的最佳选择。
  • 据我记忆,您应该避免在 Scala 中使用 Throwable。所以parseInt 应该赶上Exception: case _: Exception =&gt; ...
  • @r0estir0bbe 确实应该是case NonFatal(e) =&gt;,但我认为它与主题无关。
【解决方案2】:

您想隔离 test* 方法并停止使用推导式!

假设(无论出于何种原因)scalaz 不是您的选择...无需添加依赖项即可完成。

与许多 scalaz 示例不同,这是一个库不会比“常规”scala 更能减少冗长的示例:

def testPerson(person: Person): Either[List[String], Person] = {
  val name  = testPersonName(person.name)
  val addr  = testAddress(person.address)
  val phone = testPhone(person.phone)

  val errors = List(name, addr, phone) collect { case Left(err) => err }

  if(errors.isEmpty) Right(person) else Left(errors)      
}

【讨论】:

    【解决方案3】:

    正如@TravisBrown 告诉你的那样,因为理解并没有真正与错误累积混合。事实上,当您不想进行细粒度的错误控制时,通常会使用它们。

    A for comprehension 将在发现第一个错误时“短路”自身,这几乎总是你想要的。

    您正在做的坏事是使用String 对异常进行流控制。您应该始终使用Either[Exception, Whatever] 并使用scala.util.control.NoStackTracescala.util.NonFatal 微调日志记录。

    还有更好的选择,特别是:

    scalaz.EitherTscalaz.ValidationNel

    更新:(这是不完整的,我不知道你到底想要什么)。您有比匹配更好的选择,例如getOrElserecover

    def testPerson(person: Person): Person = {
      val attempt = Try {
        val pn = testPersonName(person.name)
        val a = testAddress(person.address)
        testPhone(person.phone)
      }
      attempt match {
        case Success(person) => //..
        case Failure(exception) => //..
      }
    }
    

    【讨论】:

    • Either[Exception, Whatever] 基本上等同于Try[Watever]
    • @dmitry - 对于“基本上”的给定定义
    • @dmitry 不一定,您不能总是使用Try 进行流量控制。这两种构造有非常具体的使用场景。
    • @dmitry 和@flavian 那么,我可以在这个例子中使用Try 代替Either 吗?
    • @Michael 你可以让你的代码更简洁一些,但是Try 仍然没有任何内置功能可以专门帮助聚合错误。这可能会更难,因为您会发现自己有一个代表多个错误的Exception
    【解决方案4】:

    Scala 2.13 开始,我们可以在Eithers 中的partitionMapList 中根据Either 的一侧对元素进行分区。

    // def testName(pn: Name): Either[String, Name] = ???
    // def testAddress(a: Address): Either[String, Address] = ???
    // def testPhone(p: Phone): Either[String, Phone] = ???
    List(testName(Name("name")), testAddress(Address("address")), testPhone(Phone("phone")))
      .partitionMap(identity) match {
        case (Nil, List(name: Name, address: Address, phone: Phone)) =>
          Right(Person(name, address, phone))
        case (left, _) =>
          Left(left)
      }
    // Either[List[String], Person] = Left(List("wrong name", "wrong phone"))
    // or
    // Either[List[String], Person] = Right(Person(Name("name"), Address("address"), Phone("phone")))
    

    如果左侧为空,则没有元素是 Left,因此我们可以从 Right 元素中构建一个 Person

    否则,我们返回Left 值中的Left List


    中间步骤详情(partitionMap):

    List(Left("bad name"), Right(Address("addr")), Left("bad phone"))
      .partitionMap(identity)
    // (List[String], List[Any]) = (List("bad name", "bad phone"), List[Any](Address("addr")))
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-09-07
      相关资源
      最近更新 更多