Scala中的方法参数验证,用于理解和monad

Seb*_*ber 33 monads scala either for-comprehension

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

谁能告诉我该怎么办?

我正在尝试这样的事情:

  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) )
    }
  }
Run Code Online (Sandbox Code Playgroud)

Tra*_*own 87

如果您愿意使用Scalaz,它有一些工具可以使这种任务更加方便,包括一个新Validation类和一些有用的右偏置类型实例scala.Either.我将举一个这里的例子.

累积错误 Validation

首先是我们的Scalaz导入(注意我们必须隐藏scalaz.Category以避免名称冲突):

import scalaz.{ Category => _, _ }
import syntax.apply._, syntax.std.option._, syntax.validation._
Run Code Online (Sandbox Code Playgroud)

我在这个例子中使用Scalaz 7.您需要进行一些小的更改才能使用6.

我假设我们有这个简化的模型:

case class User(name: String)
case class Category(user: User, parent: Category, name: String, desc: String)
Run Code Online (Sandbox Code Playgroud)

接下来,我将定义以下验证方法,如果您转向不涉及检查空值的方法,则可以轻松调整该方法:

def nonNull[A](a: A, msg: String): ValidationNel[String, A] =
   Option(a).toSuccess(msg).toValidationNel
Run Code Online (Sandbox Code Playgroud)

Nel部分代表"非空列表",a ValidationNel[String, A]基本上与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)
Run Code Online (Sandbox Code Playgroud)

请注意,这Validation[Whatever, _]不是monad(例如,这里讨论的原因),但是它ValidationNel[String, _]是一个应用程序仿函数,当我们"升级" Category.apply它时,我们在这里使用这个事实.有关applicative functors的更多信息,请参阅下面的附录.

现在,如果我们写这样的东西:

val result: ValidationNel[String, Category] = 
  buildCategory(User("mary"), null, null, "Some category.")
Run Code Online (Sandbox Code Playgroud)

我们会因累积的错误而失败:

Failure(
 NonEmptyList(
   Parent category is mandatory for a normal category,
   Name is mandatory for a normal category
  )
)
Run Code Online (Sandbox Code Playgroud)

如果所有参数都已检出,我们将Success使用Category值来代替.

失败快 Either

使用applicative functor进行验证的一个方便的事情是,您可以轻松地更换处理错误的方法.如果你想在第一次失败而不是积累它们,你基本上可以改变你的nonNull方法.

我们确实需要一组略有不同的导入:

import scalaz.{ Category => _, _ }
import syntax.apply._, std.either._
Run Code Online (Sandbox Code Playgroud)

但是没有必要改变上面的案例类.

这是我们新的验证方法:

def nonNull[A](a: A, msg: String): Either[String, A] = Option(a).toRight(msg)
Run Code Online (Sandbox Code Playgroud)

几乎与上面的相同,除了我们使用Either而不是ValidationNEL,并且Scalaz提供的默认应用程序仿函数实例Either不会累积错误.

这就是我们需要做的就是获得所需的快速失败行为 - 我们的buildCategory方法不需要进行任何更改.现在,如果我们写这个:

val result: Either[String, Category] =
  buildCategory(User("mary"), null, null, "Some category.")
Run Code Online (Sandbox Code Playgroud)

结果将只包含第一个错误:

Left(Parent category is mandatory for a normal category)
Run Code Online (Sandbox Code Playgroud)

正如我们想要的那样.

附录:应用仿函数的快速入门

假设我们有一个带有单个参数的方法:

def incremented(i: Int): Int = i + 1
Run Code Online (Sandbox Code Playgroud)

并且还假设我们想要将此方法应用于某些人x: Option[Int]并获得Option[Int]回报.事实上,这Option是一个仿函数,因此提供了一种map方法,使这很容易:

val xi = x map incremented
Run Code Online (Sandbox Code Playgroud)

我们已经" 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.
Run Code Online (Sandbox Code Playgroud)

事实上,这Option是一个仿函数是不够的.然而,它是monad的事实是,我们可以flatMap用来获得我们想要的东西:

val xy: Option[Int] = x.flatMap(xv => y.map(add(xv, _)))
Run Code Online (Sandbox Code Playgroud)

或者,等效地:

val xy: Option[Int] = for { xv <- x; yv <- y } yield add(xv, yv)
Run Code Online (Sandbox Code Playgroud)

但从某种意义上说,Option这种行动的单一性是过度的.有一个更简单的抽象 - 称为应用程序仿函数 - 介于仿函数和monad之间,它提供了我们需要的所有机制.

请注意,它正式意义上介于两者之间:每个monad都是一个applicative functor,每个applicative functor都是一个functor,但不是每个applicative functor都是monad等.

Scalaz为我们提供了一个applicative functor实例Option,因此我们可以编写以下代码:

import scalaz._, std.option._, syntax.apply._

val xy = (x |@| y)(add)
Run Code Online (Sandbox Code Playgroud)

语法有点奇怪,但这个概念并不比上面的functor或monad例子复杂 - 我们只是add进入了applicative functor.如果我们有一个f带有三个参数的方法,我们可以编写以下内容:

val xyz = (x |@| y |@| z)(f)
Run Code Online (Sandbox Code Playgroud)

等等.

那么,当我们有monad时,为什么还要使用applicative functor呢?首先,根本不可能为我们想要使用的一些抽象提供monad实例 - 这Validation是一个完美的例子.

第二(和与此相关的),它只是一个坚实的发展实践中使用强大的抽象,将完成这项工作.原则上,这可能允许优化,否则这是不可能的,但更重要的是它使我们编写的代码更可重用.


Nik*_*kov 8

我完全支持Ben James建议为null生成api创建一个包装器.但是在编写包装器时你仍会遇到同样的问题.所以这是我的建议.

monad为什么要理解?一个过度复杂的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")
    )
Run Code Online (Sandbox Code Playgroud)

或者,如果您坚持让错误消息存储参数的名称,您可以执行以下操作,这将需要更多样板:

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(", ")
      )
    )
  }
Run Code Online (Sandbox Code Playgroud)

  • 当你进行这种验证时,想要累积错误并不罕见(这样你就可以告诉用户所有错误的东西),而这种简化的方法并不能真正支持这种错误. (2认同)

ms-*_*-tg 5

如果您喜欢@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)))
}
Run Code Online (Sandbox Code Playgroud)

换句话说,如果所有的任一个都是权利,这个函数将返回一个包含您的类别的权利,或者如果一个或多个错误是左,它将返回一个包含所有错误的列表的左。

请注意可以说更像 Scala 和更少的 Haskell 语法,以及一个更小的库;)