web-dev-qa-db-ja.com

理解とモナドのためのScalaでのメソッドパラメータの検証

メソッドのパラメーターの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) )
    }
  }
34

Scalaz を使用する場合は、新しいValidationクラスや、いくつかの便利な右バイアス型クラスインスタンスなど、この種のタスクをより便利にするツールがいくつかあります。昔ながらのscala.Eitherの場合。ここでそれぞれの例を示します。

Validationでエラーを累積する

まず、Scalazのインポートについて(名前の競合を避けるためにscalaz.Categoryを非表示にする必要があることに注意してください):

import scalaz.{ Category => _, _ }
import syntax.apply._, syntax.std.option._, syntax.validation._

この例ではScalaz7を使用しています。 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の部分は「空でないリスト」を表し、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, _]はモナドではないことに注意してください(たとえば、説明されている理由により ここ )が、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が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ファンクターに「持ち上げ」ました。つまり、基本的に、IntからIntへの関数マッピングを1つのマッピングOption[Int]から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がファンクターであるという事実は十分ではありません。ただし、これがモナドであるという事実は、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のモナドネスはこ​​の操作ではやり過ぎです。ファンクターとモナドの中間にあり、必要なすべての機械を提供する、applicativeファンクターと呼ばれるより単純な抽象化があります。

正式な意味ではin-betweenであることに注意してください:すべてのモナドは適用ファンクターであり、すべての適用ファンクターはファンクターですが、すべての適用ファンクターがモナドなど.

Scalazは、Optionの適用可能なファンクターインスタンスを提供するため、次のように記述できます。

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

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

構文は少し奇妙ですが、概念は上記のファンクターまたはモナドの例よりも複雑ではありません。適用可能なファンクターにaddを持ち上げているだけです。 3つの引数を持つメソッドfがある場合、次のように書くことができます。

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

等々。

では、モナドがあるのに、なぜアプリケーションファンクターに煩わされるのでしょうか。まず第一に、私たちが扱いたい抽象化のいくつかにモナドインスタンスを提供することは単に不可能です— Validationは完璧な例です。

第二に(そして関連して)、仕事を成し遂げるのは最小の強力な抽象化を使用することは確かな開発慣行です。原則として、これにより、他の方法では不可能な最適化が可能になる場合がありますが、さらに重要なことに、これにより、作成するコードがより再利用可能になります。

87
Travis Brown

Nullを生成するAPIのラッパーを作成するというBenJamesの提案を完全にサポートします。ただし、そのラッパーを作成するときにも同じ問題が発生します。だからここに私の提案があります。

なぜモナドはなぜ理解のために?過度に複雑な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(", ")
      )
    )
  }
8
Nikita Volkov

@Travis Brownの回答のアプリケーションファンクターアプローチは好きだが、Scalaz構文が気に入らない場合、またはScalazを使用したくない場合は、標準ライブラリを強化する単純なライブラリを次に示します。どちらのクラスもアプリケーションとして機能します。ファンクターの検証: https://github.com/youdevise/ethervalidation

例えば:

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風の構文が少なく、ライブラリが小さいことに注意してください;)

4
ms-tg

次の迅速で汚いものでどちらかを完了したとしましょう:

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

大したことではありません。

0
wiki1000