Ответ 1
Если вы хотите использовать Scalaz, у него есть несколько инструментов, которые делают эту задачу более удобной, в том числе новый класс Validation
и некоторые полезные экземпляры класса класса с прямым смещением для простого старого scala.Either
. Я приведу пример каждого из них.
Накопление ошибок с помощью Validation
Сначала для нашего импорта Scalaz (обратите внимание, что нам нужно скрыть scalaz.Category
, чтобы избежать конфликта имен):
import scalaz.{ Category => _, _ }
import syntax.apply._, syntax.std.option._, syntax.validation._
Я использую Scalaz 7 для этого примера. Вам нужно будет внести некоторые незначительные изменения в использование 6.
Я предполагаю, что у нас есть эта упрощенная модель:
case class User(name: String)
case class Category(user: User, parent: Category, name: String, desc: String)
Далее я определяю следующий метод проверки, который вы можете легко адаптировать, если перейти к подходу, который не включает проверку нулевых значений:
def nonNull[A](a: A, msg: String): ValidationNel[String, A] =
Option(a).toSuccess(msg).toValidationNel
Часть Nel
означает "непустой список", a ValidationNel[String, A]
по существу совпадает с Either[List[String], A]
.
Теперь мы используем этот метод для проверки наших аргументов:
def buildCategory(user: User, parent: Category, name: String, desc: String) = (
nonNull(user, "User is mandatory for a normal category") |@|
nonNull(parent, "Parent category is mandatory for a normal category") |@|
nonNull(name, "Name is mandatory for a normal category") |@|
nonNull(desc, "Description is mandatory for a normal category")
)(Category.apply)
Обратите внимание, что Validation[Whatever, _]
не является монадой (например, по обсуждаемым причинам здесь), но ValidationNel[String, _]
- прикладной функтор, и мы используя этот факт здесь, когда мы "поднимем" Category.apply
на него. Дополнительную информацию об аппликативных функторах см. В приложении ниже.
Теперь, если мы напишем что-то вроде этого:
val result: ValidationNel[String, Category] =
buildCategory(User("mary"), null, null, "Some category.")
Мы получим сбой с накопленными ошибками:
Failure(
NonEmptyList(
Parent category is mandatory for a normal category,
Name is mandatory for a normal category
)
)
Если все аргументы были проверены, вместо этого у нас будет Success
с Category
.
Не удалось быстро с Either
Одним из удобных способов использования аппликативных функторов для проверки является легкость, с которой вы можете поменять свой подход к обработке ошибок. Если вы хотите свалить первый, а не накапливать их, вы можете просто изменить свой метод nonNull
.
Нам нужен немного другой набор импорта:
import scalaz.{ Category => _, _ }
import syntax.apply._, std.either._
Но нет необходимости менять классы case выше.
Здесь наш новый метод проверки:
def nonNull[A](a: A, msg: String): Either[String, A] = Option(a).toRight(msg)
Почти идентичен приведенному выше, за исключением того, что мы используем Either
вместо ValidationNEL
, а пример прикладного функционала по умолчанию, который Scalaz предоставляет для Either
, не накапливает ошибки.
Это все, что нам нужно сделать, чтобы получить желаемое быстрое выполнение - никакие изменения не нужны нашему методу buildCategory
. Теперь, если мы напишем это:
val result: Either[String, Category] =
buildCategory(User("mary"), null, null, "Some category.")
Результат будет содержать только первую ошибку:
Left(Parent category is mandatory for a normal category)
Точно так, как мы хотели.
Приложение: Краткое введение в аппликативные функторы
Предположим, что у нас есть метод с единственным аргументом:
def incremented(i: Int): Int = i + 1
Предположим также, что мы хотим применить этот метод к некоторому x: Option[Int]
и получить обратно Option[Int]
. Тот факт, что Option
является функтором и поэтому обеспечивает метод map
, делает это простым:
val xi = x map incremented
Мы "подняли" incremented
в функтор Option
; то есть мы по существу изменили отображение функции Int
на Int
на одно отображение Option[Int]
на Option[Int]
(хотя синтаксис мутирует немного вверх - метафору "лифтинга" гораздо яснее на языке, таком как Haskell).
Теперь предположим, что мы хотим применить следующий метод add
к x
и y
аналогичным образом.
def add(i: Int, j: Int): Int = i + j
val x: Option[Int] = users.find(_.name == "John").map(_.age)
val y: Option[Int] = users.find(_.name == "Mary").map(_.age) // Or whatever.
Тот факт, что Option
является функтором, недостаточно. Тот факт, что это монада, однако, мы можем использовать flatMap
, чтобы получить то, что хотим:
val xy: Option[Int] = x.flatMap(xv => y.map(add(xv, _)))
Или, что эквивалентно:
val xy: Option[Int] = for { xv <- x; yv <- y } yield add(xv, yv)
В некотором смысле, монадность Option
является излишней для этой операции. Там более простая абстракция, называемая прикладным функтором, - это промежуточный функтор и монада, который обеспечивает все необходимое оборудование.
Обратите внимание, что это промежуточный в формальном смысле: каждая монада является прикладным функтором, каждый прикладной функтор является функтором, но не каждый прикладной функтор является монадой и т.д.
Scalaz дает нам экземпляр прикладного функтора для Option
, поэтому мы можем написать следующее:
import scalaz._, std.option._, syntax.apply._
val xy = (x |@| y)(add)
Синтаксис немного странный, но концепция не сложнее, чем примеры функтора или монады выше - мы просто поднимаем add
в аппликативный функтор. Если бы у нас был метод f
с тремя аргументами, мы могли бы написать следующее:
val xyz = (x |@| y |@| z)(f)
И так далее.
Так зачем вообще заниматься аппликативными функторами, когда у нас есть монады? Во-первых, просто невозможно предоставить экземпляры monad для некоторых абстракций, с которыми мы хотим работать - Validation
- прекрасный пример.
Второй (и связанный с этим), это просто твердая практика разработки, чтобы использовать наименее мощную абстракцию, которая выполнит свою работу. В принципе это может позволить оптимизацию, которая в противном случае была бы невозможна, но что более важно, это делает код, который мы пишем, более многократно используется.