Как накапливать ошибки в Либо?
Предположим, у меня есть несколько классов и функций для их проверки:
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] = ...
Теперь я определяю новый класс case 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]
. Как я могу это сделать?
Ответы
Ответ 1
Scala for
-познания (которые desugar для комбинации вызовов 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")
. Тогда parseInt
, очевидно, завершится неудачей, а это значит, что мы не можем иметь значение для i
, что означает, что мы не могли бы перейти к шагу checkNonzero(i)
в последовательности.
В вашем случае ваши вычисления не имеют такой зависимости, но абстракция, которую вы используете (монадическая последовательность), не знает этого. Вам нужен тип Either
, который не является монадическим, но применим. См. мой ответ здесь для получения подробной информации о различии.
Например, вы можете написать следующее с 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
будет немного чище.
Ответ 2
Вы хотите изолировать методы test*
и прекратить использование понимания!
Предполагая (по какой-либо причине), что 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 для понимания будет "короткозамкнуто" при первой обнаруженной ошибке, и это почти всегда то, что вы хотите.
Плохая вещь, которую вы делаете, заключается в использовании 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) => //..
}
}
Ответ 4
Начиная с Scala 2.13
, мы можем Either[A1,A2]):(CC[A1],CC[A2]) rel="nofollow noreferrer"> 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
не осталось ни одного элемента, и поэтому мы можем построить Person
из Right
элементов.
В противном случае мы возвращаем Left
List
Left
значений.
Детали промежуточного шага (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")))