35

我正在尝试验证无效方法的参数,但我没有找到解决方案......

有人可以告诉我该怎么做吗?

我正在尝试这样的事情:

  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) )
    }
  }
4

4 回答 4

88

如果你愿意使用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

我们已经“提升”incrementedOption函子中;也就是说,我们本质上已经将一个函数映射Intto更改Int为一个映射Option[Int]to Option[Int](尽管语法有点混乱——“提升”隐喻在 Haskell 之类的语言中更清晰)。

现在假设我们想以类似的方式应用以下add方法。xy

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 就是一个完美的例子。

其次(也是相关的),使用最强大的抽象来完成工作只是一种可靠的开发实践。原则上,这可能会实现原本不可能的优化,但更重要的是,它使我们编写的代码更具可重用性。

于 2012-09-06T22:00:22.653 回答
9

我完全支持 Ben James 的建议,即为产生空值的 api 做一个包装器。但是在编写该包装器时,您仍然会遇到同样的问题。所以这是我的建议。

为什么单子为什么要理解?一个过度复杂的国际海事组织。您可以这样做:

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(", ")
      )
    )
  }
于 2012-09-06T21:49:01.127 回答
5

如果您喜欢@Travis Brown 答案的应用函子方法,但您不喜欢 Scalaz 语法或者只是不想使用 Scalaz,那么这里有一个简单的库,它丰富了标准库 Either 类以充当应用程序函子验证: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)))
}

换句话说,如果所有的 Either 都是 Right,则此函数将返回包含您的 Category 的 Right,或者如果一个或多个是 Left,它将返回包含所有 Errors 的 List 的 Left。

请注意可以说是更多的 Scala 风格和更少的 Haskell 风格的语法,以及更小的库;)

于 2013-02-27T23:47:24.443 回答
0

让我们假设您已经使用以下快速而肮脏的东西完成了 Either:

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 )
   ))

没有大碍。

于 2014-11-12T15:37:29.750 回答