35

メソッドのパラメーターのnullを検証しようとしていますが、解決策が見つかりません...

誰かが私にやり方を教えてもらえますか?

私はこのようなことを試みています:

  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)

次に、次の検証方法を定義します。これは、null 値のチェックを伴わないアプローチに移行する場合に簡単に適応できます。

def nonNull[A](a: A, msg: String): ValidationNel[String, A] =
   Option(a).toSuccess(msg).toValidationNel

部分は「Nel空でないリスト」を表し、 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, _]で説明した理由により)、アプリカティブ ファンクターであることに注意してください。ここで "リフト" するときにその事実を使用しています。アプリカティブ ファンクターの詳細については、以下の付録を参照してください。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

で速く失敗するEither

検証にアプリケーション ファンクターを使用する際の便利な点の 1 つは、エラー処理へのアプローチを簡単に交換できることです。それらを蓄積するのではなく、最初に失敗したい場合は、基本的に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 が提供するデフォルトの applicative functor インスタンス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つまり、本質的に、関数のマッピングIntInt1 つのマッピングOption[Int]に変更しましたOption[Int](ただし、構文が少し混乱していますが、"持ち上げる" メタファーは、Haskell のような言語でより明確になります)。

次に、同様の方法でandに次の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ファンクタであるという事実だけでは十分ではありません。しかし、それがモナドであるという事実はそうであり、私たちが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この操作では のモナド性は過剰です。アプリカティブ ファンクターと呼ばれる、より単純な抽象化があります。これは、ファンクターとモナドの間にあり、必要なすべての機構を提供します。

形式的な意味では中間であることに注意してください: すべてのモナドはアプリカティブ ファンクターであり、すべてのアプリカティブ ファンクターはファンクターですが、すべてのアプリカティブ ファンクターがモナドであるとは限りません。

Scalaz は のアプリカティブ ファンクター インスタンスを提供するOptionので、次のように記述できます。

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

val xy = (x |@| y)(add)

構文は少し奇妙ですが、概念は上記のファンクターやモナドの例ほど複雑ではありません —addアプリケーション ファンクターに持ち込んでいるだけです。f3 つの引数を持つメソッドがある場合、次のように記述できます。

val xyz = (x |@| y |@| z)(f)

等々。

では、モナドがあるのに、なぜ Applicative Functor を気にする必要があるのでしょうか? まず第一に、処理したい抽象化のいくつかに対してモナドのインスタンスを提供することは単純に不可能です — <code>Validation は完璧な例です。

2 番目に (そして関連して)、仕事を成し遂げる最も強力でない抽象化を使用することは、堅実な開発プラクティスです。原則として、これにより、他の方法では不可能な最適化が可能になる可能性がありますが、さらに重要なことは、記述したコードがより再利用しやすくなるということです。

于 2012-09-06T22:00:22.653 に答える
9

私は、null を生成する API のラッパーを作成するという Ben James の提案を完全に支持します。ただし、そのラッパーを作成するときにも同じ問題が発生します。だからここに私の提案があります。

なぜモナドなのか、なぜ理解するのか? 複雑すぎる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(", ")
      )
    )
  }
于 2012-09-06T21:49:01.127 に答える
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)))
}

つまり、この関数は、すべてのどちらかが権利である場合はカテゴリを含む権利を返し、1 つ以上が左である場合はすべてのエラーのリストを含む左を返します。

ほぼ間違いなく 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))
    }
  }
}
}}

次のいずれかを返す検証関数を検討してください。

  def whenNone (value: Option[String],msg:String): Either[String,String] = 
      if (value isEmpty) Left(msg) else Right(value.get)

タプルを返すカリー化されたコンストラクター:

  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 に答える