如果你愿意使用
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)
接下来我将定义以下验证方法,如果您采用不涉及检查 null 值的方法,您可以轻松地进行调整:
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)对象。
使用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”映射到“Int”的函数转换为一个将“Option[Int]”映射到“Option[Int]”的函数(尽管语法有些混乱——在像Haskell这样的语言中,“lifting”隐喻更清晰)。
现在假设我们想以类似的方式将以下“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)
事实上,
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
就是一个完美的例子。
其次(并且相关),使用能够完成任务的最不强大的抽象只是一种可靠的开发实践。原则上,这可能允许进行否则不可能的优化,但更重要的是使我们编写的代码更具可重用性。
null
,而是在适当的情况下使用Option[..]
非常有用。 - PetrOption
。如果您感兴趣,可以查找如何使用Scalaz库来实现此操作。否则,只需在for-comprehension中应用该方法即可。 - Ben James