簡體   English   中英

Scala中的方法參數驗證,用於理解和單子

[英]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

我們已經“解禁” incrementedOption函子; 也就是說,我們已經基本上改變了功能映射IntInt成一個映射Option[Int]Option[Int]雖然語法muddies說了一下,在“提升”的比喻是象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的單調對於此操作來說是過大的。 在函子和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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM