[英]Method parameters validation in Scala, with for comprehension and monads
我正在嘗試驗證方法的參數是否為空,但找不到解決方案...
有人可以告訴我該怎么做嗎?
我正在嘗試這樣的事情:
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) )
}
}
如果您願意使用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, _]
是Validation[Whatever, _]
都不是monad(例如,出於此處討論的原因),但是ValidationNel[String, _]
是可應用的函子,當我們“提升” Category.apply
時,我們在這里使用了這一事實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
)
)
如果所有參數都已簽出,那么我們將獲得帶有Category
值的Success
。
Either
失敗, 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]
雖然語法muddies說了一下,在“提升”的比喻是象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
的單調對於此操作來說是過大的。 在函子和monad之間有一個更簡單的抽象(稱為應用函子),它提供了我們需要的所有機械。
請注意,這是介於兩者之間的形式:每個monad是一個應用函子,每個applicative函子都是函子,但不是每個applicative函子都是monad,依此類推。
Scalaz為Option
提供了一個應用函子實例,因此我們可以編寫以下代碼:
import scalaz._, std.option._, syntax.apply._
val xy = (x |@| y)(add)
語法有點奇怪,但是概念並不比上面的functor或monad示例更復雜-我們只是將add
提升到應用的functor中。 如果我們有一個帶有三個參數的方法f
,則可以編寫以下代碼:
val xyz = (x |@| y |@| z)(f)
等等。
那么,當我們有了單子時,為什么還要煩惱所有應用函子呢? 首先,根本不可能為我們要使用的某些抽象提供monad實例- Validation
是一個完美的例子。
其次(以及相關),使用最不強大的抽象來完成工作只是一種可靠的開發實踐。 原則上,這可能允許進行其他優化,但更重要的是,它使我們編寫的代碼更具可重用性。
我完全支持Ben James的建議,為產生null的api做包裝。 但是編寫該包裝器時仍然會遇到相同的問題。 所以這是我的建議。
為什么單子為什么要理解? 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(", ")
)
)
}
如果您喜歡@Travis Brown的答案的應用函子方法,但是不喜歡Scalaz語法,或者只是不想使用Scalaz,這里有一個簡單的庫,它豐富了標准庫Either類以充當應用程序函子驗證: https : //github.com/youdevise/twovalidation
例如:
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”,則此函數將返回包含您的類別的“ Right”,如果一個或多個為“ Lefts”,則此函數將返回包含所有錯誤列表的“ Left”。
請注意,可以說更多的Scala式語法和更少的Haskell式語法,以及較小的庫;)
讓我們假設您用以下快速又骯臟的東西完成了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)
一個Cururfied構造函數,返回一個元組:
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 )
))
沒什么大不了的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.