如果你愿意使用Scalaz,它有一些工具可以让这种任务更方便,包括一个新Validation
类和一些用于普通 old 的有用的右偏类型类实例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 与 aValidationNel[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, _]
不是一个 monad(例如,出于此处讨论的原因),而是一个应用函子,当我们“提升”到它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
值的 a。
快速失败Either
使用应用函子进行验证的方便之处之一是您可以轻松地更换处理错误的方法。如果你想在第一次失败而不是累积它们,你基本上可以改变你的nonNull
方法。
我们确实需要一组稍微不同的导入:
import scalaz.{ Category => _, _ }
import syntax.apply._, std.either._
但是没有必要改变上面的案例类。
这是我们的新验证方法:
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
to更改Int
为一个映射Option[Int]
to 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
是不够的。然而,它是一个 monad 的事实是,我们可以使用它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
对于这个操作来说, monadness 是多余的。有一个更简单的抽象——称为应用函子——介于函子和单子之间,它提供了我们需要的所有机制。
请注意,它在形式上介于两者之间:每个 monad 都是应用函子,每个应用函子都是函子,但并非每个应用函子都是 monad,等等。
Scalaz 为我们提供了一个应用函子实例Option
,因此我们可以编写以下代码:
import scalaz._, std.option._, syntax.apply._
val xy = (x |@| y)(add)
语法有点奇怪,但概念并不比上面的仿函数或 monad 示例复杂——我们只是提升add
到应用仿函数。如果我们有一个f
带有三个参数的方法,我们可以编写以下代码:
val xyz = (x |@| y |@| z)(f)
等等。
那么,当我们有 monad 时,为什么还要操心应用函子呢?首先,根本不可能为我们想要使用的一些抽象提供 monad 实例——<code>Validation 就是一个完美的例子。
其次(也是相关的),使用最不强大的抽象来完成工作只是一种可靠的开发实践。原则上,这可能会实现原本不可能的优化,但更重要的是,它使我们编写的代码更具可重用性。