我正在尝试验证无效方法的参数,但我找不到解决方案......
有人可以告诉我该怎么做吗?
我正在尝试这样的事情:
def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[Error,Category] = {
val errors: Option[String] = for {
_ <- Option(user).toRight("User is mandatory for a normal category").right
_ <- Option(parent).toRight("Parent category is mandatory for a normal category").right
_ <- Option(name).toRight("Name is mandatory for a normal category").right
errors : Option[String] <- Option(description).toRight("Description is mandatory for a normal category").left.toOption
} yield errors
errors match {
case Some(errorString) => Left( Error(Error.FORBIDDEN,errorString) )
case None => Right( buildTrashCategory(user) )
}
}
答案 0 :(得分:86)
如果您愿意使用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, _]
不是monad(例如,出于here所述的原因),但ValidationNel[String, _]
是一个应用函子,我们在这里使用这个事实“抬起”Category.apply
进入它。有关applicative functors的更多信息,请参阅下面的附录。
现在,如果我们写这样的话:
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
使用applicative functor进行验证的一个方便的事情是,您可以轻松地更换处理错误的方法。如果你想在第一次失败而不是积累它们,你基本上可以改变你的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这样的语言中更加清晰。
现在假设我们想以类似的方式将以下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对于此操作来说是过度的。有一个更简单的抽象 - 称为 applicative 仿函数 - 它位于仿函数和monad之间,它提供了我们需要的所有机制。
请注意,正式意义上的介于之间:每个monad都是一个applicative functor,每个applicative functor都是一个functor,但不是每个applicative functor都是monad等。
Scalaz为Option
提供了一个应用程序仿函数实例,因此我们可以编写以下内容:
import scalaz._, std.option._, syntax.apply._
val xy = (x |@| y)(add)
语法有点奇怪,但这个概念并不比上面的functor或monad例子复杂 - 我们只是将add
提升到applicative functor中。如果我们有一个带有三个参数的方法f
,我们可以编写以下内容:
val xyz = (x |@| y |@| z)(f)
等等。
那么,当我们有monad时,为什么还要使用applicative functor呢?首先,根本不可能为我们想要使用的一些抽象提供monad实例 - Validation
就是一个很好的例子。
第二个(相关地),使用最少强大的抽象来完成工作只是一个可靠的开发实践。原则上,这可能允许无法实现的优化,但更重要的是,它使我们编写的代码更具可重用性。
答案 1 :(得分:7)
我完全支持Ben James建议为null生成api创建一个包装器。但是在编写包装器时你仍会遇到同样的问题。所以这是我的建议。
为什么monads为什么要理解?一个过度复杂的IMO。这是你如何做到的:
def buildNormalCategory
( user: User, parent: Category, name: String, description: String )
: Either[ Error, Category ]
= Either.cond(
!Seq(user, parent, name, description).contains(null),
buildTrashCategory(user),
Error(Error.FORBIDDEN, "null detected")
)
或者,如果您坚持让错误消息存储参数的名称,您可以执行以下操作,这将需要更多样板:
def buildNormalCategory
( user: User, parent: Category, name: String, description: String )
: Either[ Error, Category ]
= {
val nullParams
= Seq("user" -> user, "parent" -> parent,
"name" -> name, "description" -> description)
.collect{ case (n, null) => n }
Either.cond(
nullParams.isEmpty,
buildTrashCategory(user),
Error(
Error.FORBIDDEN,
"Null provided for the following parameters: " +
nullParams.mkString(", ")
)
)
}
答案 2 :(得分:4)
如果您喜欢@Travis Brown答案的应用程序仿函数方法,但您不喜欢Scalaz语法或者只是不想使用Scalaz,这里有一个简单的库,可以丰富标准库。作为应用仿函数验证:https://github.com/youdevise/eithervalidation
例如:
import com.youdevise.eithervalidation.EitherValidation.Implicits._
def buildNormalCategory(user: User, parent: Category, name: String, description: String): Either[List[Error], Category] = {
val validUser = Option(user).toRight(List("User is mandatory for a normal category"))
val validParent = Option(parent).toRight(List("Parent category is mandatory for a normal category"))
val validName = Option(name).toRight(List("Name is mandatory for a normal category"))
Right(Category)(validUser, validParent, validName).
left.map(_.map(errorString => Error(Error.FORBIDDEN, errorString)))
}
换句话说,如果所有Eithers都是Rights,则此函数将返回包含您的Category的Right,或者如果一个或多个是Lefts,它将返回包含所有错误列表的Left。
注意可以说更多的Scala-ish和更少的Haskell-ish语法,以及更小的库;)
答案 3 :(得分:0)
让我们假设你已经完成了以下快速和肮脏的东西:
object Validation {
var errors = List[String]()
implicit class Either2[X] (x: Either[String,X]){
def fmap[Y](f: X => Y) = {
errors = List[String]()
//println(s"errors are $errors")
x match {
case Left(s) => {errors = s :: errors ; Left(errors)}
case Right(x) => Right(f(x))
}
}
def fapply[Y](f: Either[List[String],X=>Y]) = {
x match {
case Left(s) => {errors = s :: errors ; Left(errors)}
case Right(v) => {
if (f.isLeft) Left(errors) else Right(f.right.get(v))
}
}
}
}}
考虑返回Either:
的验证函数 def whenNone (value: Option[String],msg:String): Either[String,String] =
if (value isEmpty) Left(msg) else Right(value.get)
一个返回元组的curryfied构造函数:
val me = ((user:String,parent:String,name:String)=> (user,parent,name)) curried
您可以使用以下方式对其进行验证:
whenNone(None,"bad user")
.fapply(
whenNone(Some("parent"), "bad parent")
.fapply(
whenNone(None,"bad name")
.fmap(me )
))
没什么大不了的。