Если вы хотите использовать 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
означает «непустой список», а 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)
И так далее.
Так зачем вообще возиться с аппликативными функторами, когда у нас есть монады? Во-первых, просто невозможно предоставить экземпляры монад для некоторых абстракций, с которыми мы хотим работать — Validation
идеальный пример.
Во-вторых (и связанное с этим), это просто надежная практика разработки — использовать наименее мощную абстракцию, которая выполнит работу. В принципе, это может позволить оптимизацию, которая в противном случае была бы невозможна, но, что более важно, это делает код, который мы пишем, более пригодным для повторного использования.
person
Travis Brown
schedule
06.09.2012
null
в Scala и вместо этого использоватьOption[..]
, где это уместно. - person Petr   schedule 07.09.2012Option
с. Посмотрите, как это сделать с библиотекой Scalaz, если вам интересно. В противном случае просто примените метод внутри for-comprehension. - person Ben James   schedule 07.09.2012