12

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

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]。我怎样才能做到这一点 ?

4

4 回答 4

17

您想隔离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)      
}
于 2014-01-25T14:21:48.267 回答
14

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

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"). ThenparseInt显然会失败,这意味着我们无法获得 的值i,这意味着我们无法继续checkNonzero(i)执行序列中的步骤。

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

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

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在整个过程中使用会更干净一些。

于 2014-01-25T14:33:53.790 回答
4

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

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

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

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

scalaz.EitherTscalaz.ValidationNel

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

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) => //..
  }
}
于 2014-01-25T14:22:01.300 回答
0

从 开始Scala 2.13,我们可以partitionMapa Listof Eithers 以便根据元素的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,因此我们可以PersonRight元素中构建一个。

否则,我们返回一个Left ListLeft


中间步骤 ( 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")))
于 2019-07-14T10:57:08.380 回答