![](/img/trans.png)
[英]How to accumulate errors in a functional way upon validating database object?
[英]How to accumulate errors in Either?
假設我有幾個案例類和函數來測試它們:
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*
函數並返回testPerson
Either[List[String], Person]
。 我怎樣才能做到這一點 ?
您想隔離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)
}
Scala 的for
-comprehensions(對flatMap
和map
調用的組合進行脫糖)旨在允許您以這樣的方式for
monadic 計算進行排序,以便您可以在后續步驟中訪問早期計算的結果。 考慮以下:
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
的類型,它不是monadic,但它是applicative 。 有關差異的一些詳細信息,請參閱我的答案here 。
例如,您可以使用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
會更簡潔一些。
正如@TravisBrown 告訴你的那樣,理解並沒有真正與錯誤累積混合在一起。 事實上,當您不想要細粒度的錯誤控制時,通常會使用它們。
A for comprehension 會在發現第一個錯誤時“短路”自己,這幾乎總是您想要的。
您正在做的壞事是使用String
進行異常流控制。 您應該始終使用Either[Exception, Whatever]
並使用scala.util.control.NoStackTrace
和scala.util.NonFatal
微調日志記錄。
有更好的選擇,特別是:
scalaz.EitherT
和scalaz.ValidationNel
。
更新:(這是不完整的,我不知道你到底想要什么)。 您有比匹配更好的選擇,例如getOrElse
和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) => //..
}
}
從Scala 2.13
開始,我們可以partitionMap
一個List
of Either
,以便根據它們的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")))
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.