Как накапливать ошибки в Либо?

Предположим, у меня есть несколько классов и функций для их проверки:

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")))