14

非常に多くの場合、いくつかの連続した条件を検証するときに、ネストされた .map と .getOrElse が大量に発生します。

例えば:

def save() = CORSAction { request =>
  request.body.asJson.map { json =>
    json.asOpt[Feature].map { feature =>
      MaxEntitiyValidator.checkMaxEntitiesFeature(feature).map { rs =>
        feature.save.map { feature => 
          Ok(toJson(feature.update).toString)
        }.getOrElse {
          BadRequest(toJson(
            Error(status = BAD_REQUEST, message = "Error creating feature entity")
          ))
        }
      }.getOrElse {
        BadRequest(toJson(
          Error(status = BAD_REQUEST, message = "You have already reached the limit of feature.")
        )) 
      }
    }.getOrElse {
      BadRequest(toJson(
        Error(status = BAD_REQUEST, message = "Invalid feature entity")
      )) 
    }
  }.getOrElse {
    BadRequest(toJson(
      Error(status = BAD_REQUEST, message = "Expecting JSON data")
    )) 
  }
}

あなたはアイデアを得る

より明確にするための慣用的な方法があるかどうか知りたかっただけです

4

4 に答える 4

12

Noneの場合に別のメッセージを返す必要がなかった場合、これは理解のための理想的なユースケースになります。あなたの場合、Scalazで見つけることができるものとして、おそらくValidationモナドを使用したいと思うでしょう。例(http://scalaz.github.com/scalaz/scalaz-2.9.0-1-6.0/doc.sxr/scalaz/Validation.scala.html)。

関数型プログラミングでは、例外をスローするのではなく、失敗する可能性のある関数がEither [A、B]を返すようにする必要があります。ここで、慣例により、Aは失敗の場合の結果のタイプであり、Bは成功の場合の結果のタイプです。次に、Left(a)またはRight(b)と照合して、それぞれ2つのケースを処理できます。

検証モナドは、拡張されたEither [A、B]と考えることができます。この場合、後続の関数を検証に適用すると、結果または実行チェーンの最初の失敗が発生します。

sealed trait Validation[+E, +A] {
  import Scalaz._

  def map[B](f: A => B): Validation[E, B] = this match {
    case Success(a) => Success(f(a))
    case Failure(e) => Failure(e)
  }

  def foreach[U](f: A => U): Unit = this match {
    case Success(a) => f(a)
    case Failure(e) =>
  }

  def flatMap[EE >: E, B](f: A => Validation[EE, B]): Validation[EE, B] = this match {
    case Success(a) => f(a)
    case Failure(e) => Failure(e)
  }

  def either : Either[E, A] = this match {
    case Success(a) => Right(a)
    case Failure(e) => Left(e)
  }

  def isSuccess : Boolean = this match {
    case Success(_) => true
    case Failure(_) => false
  }

  def isFailure : Boolean = !isSuccess

  def toOption : Option[A] = this match {
    case Success(a) => Some(a)
    case Failure(_) => None
  }


}

final case class Success[E, A](a: A) extends Validation[E, A]
final case class Failure[E, A](e: E) extends Validation[E, A]

これで、Validationモナドを使用して3つの検証レイヤーにコードをリファクタリングできます。基本的に、マップを次のような検証に置き換える必要があります。

def jsonValidation(request:Request):Validation[BadRequest,String] = request.asJson match {
   case None => Failure(BadRequest(toJson(
      Error(status = BAD_REQUEST, message = "Expecting JSON data")
    )
   case Some(data) => Success(data)
}

def featureValidation(validatedJson:Validation[BadRequest,String]): Validation[BadRequest,Feature] = {
validatedJson.flatMap {
  json=> json.asOpt[Feature] match {
    case Some(feature)=> Success(feature)
    case None => Failure( BadRequest(toJson(
      Error(status = BAD_REQUEST, message = "Invalid feature entity")
        )))
  }
}

}

そして、あなたはそれらを次のように連鎖させますfeatureValidation(jsonValidation(request))

于 2012-08-13T07:18:34.760 に答える
3

これは、モナドを使用してコードをクリーンアップできる典型的な例です。たとえばBox、まったく関連付けられていないLift の を使用できますLift。次に、コードは次のようになります。

requestBox.flatMap(asJSON).flatMap(asFeature).flatMap(doSomethingWithFeature)

ここasJsonで、 はリクエストから aBox[JSON]への関数であり、は a から他の へasFeatureの関数です。ボックスには値を含めることができます。この場合、 flatMap はその値で関数を呼び出します。または、 のインスタンスにすることができ、その場合は渡された関数を呼び出しません。FeatureBoxFailureflatMap

コンパイルできるコード例を投稿していれば、コンパイルできる回答を投稿できたはずです。

于 2012-08-13T07:11:43.727 に答える
3

私はこれを試して、提出されたコード サンプルを (文字通りではないにしてもスタイルで) より一貫性のあるものに適合させるために、パターン マッチングが何らかの方法で提供されるかどうかを確認しました。

object MyClass {

  case class Result(val datum: String)
  case class Ok(val _datum: String) extends Result(_datum)
  case class BadRequest(_datum: String) extends Result(_datum)

  case class A {}
  case class B(val a: Option[A])
  case class C(val b: Option[B])
  case class D(val c: Option[C])

  def matcher(op: Option[D]) = {
    (op,
     op.getOrElse(D(None)).c,
     op.getOrElse(D(None)).c.getOrElse(C(None)).b,
     op.getOrElse(D(None)).c.getOrElse(C(None)).b.getOrElse(B(None)).a
    ) match {
      case (Some(d), Some(c), Some(b), Some(a)) => Ok("Woo Hoo!")
      case (Some(d), Some(c), Some(b), None)    => BadRequest("Missing A")
      case (Some(d), Some(c), None,    None)    => BadRequest("Missing B")
      case (Some(d), None,    None,    None)    => BadRequest("Missing C")
      case (None,    None,    None,    None)    => BadRequest("Missing D")
      case _                                    => BadRequest("Egads")
    }
  }
}

明らかに、これをより最適に記述する方法があります。これは、読者の演習として残されています。

于 2012-08-13T07:32:46.353 に答える
0

理解のために使用するというEdmondoの提案には同意しますが、検証ライブラリの使用に関する部分には同意しません(少なくとも、2012年以降にscala標準ライブラリに追加された新機能を考えると、もはやそうではありません)。scala での私の経験から、標準的な lib で適切なステートメントを考え出すのに苦労している開発者は、cats や scalaz のような libs を使用すると、最悪の場合も同じことをすることになります。同じ場所ではないかもしれませんが、理想的には、単に移動するのではなく、問題を解決したいと考えています。

これは、理解のために書き直されたコードであり、それは scala 標準 lib の一部です。

def save() = CORSAction { request =>

  // Helper to generate the error 
  def badRequest(message: String) = Error(status = BAD_REQUEST, message)

  //Actual validation
  val updateEither = for {
    json    <- request.body.asJson.toRight(badRequest("Expecting JSON data"))
    feature <- json.asOpt[Feature].toRight(badRequest("Invalid feature entity"))
    rs <- MaxEntitiyValidator
           .checkMaxEntitiesFeature(feature)
           .toRight(badRequest("You have already reached the limit"))
  } yield toJson(feature.update).toString

  // Turn the either into an OK/BadRequest
  featureEither match {
    case Right(update) => Ok(update)
    case Left(error)   => BadRequest(toJson(error))
  }
}

説明

エラー処理

あなたがどちらについてどれだけ知っているかはわかりませんが、Edmondo によって提示された Validation または scala ライブラリの Try オブジェクトと動作がかなり似ています。これらのオブジェクトの主な違いは、機能とエラーのある動作に関するものですが、それらはすべて同じ方法でマップおよびフラット マップできます。

また、 toRightを使用して、オプションを最後に実行するのではなく、すぐに Each に変換していることもわかります。Java開発者は、物理的に可能な限り例外をスローする反射神経を持っていることがわかりますが、try catchメカニズムが扱いにくいため、ほとんどの場合そうします:成功した場合、tryブロックからデータを取得するには、それらを返す必要がありますまたは、ブロックの外で null に初期化された変数に入れます。しかし、scala の場合はそうではありません: try または either をマップすることができるため、一般に、結果が正しくないと識別されたときにすぐに結果をエラー表現に変換すると、より読みやすいコードが得られます。

理解のために

また、scala を発見した開発者は、理解に困惑することが多いことも知っています。これは、他のほとんどの言語と同じように非常に理解できます. はコレクションの反復にのみ使用されますが、スカラは、関連のない多くの型で使用できるようです. scala では、実際には関数 flatMap を呼び出すより適切な方法です。コンパイラは map または foreach を使用して最適化することを決定する場合がありますが、for を使用すると flatMap の動作が得られると想定することは正しいままです。コレクションで flatMap を呼び出すと、他の言語での for each と同じように動作するため、コレクションを処理するときに標準の for のように scala for を使用できます。ただし、正しいシグネチャを持つ flatMap の実装を提供する他のタイプのオブジェクトでも使用できます。OK/BadRequest も flatMap を実装している場合、

コレクションのように見えないものに対して for を使用するのは簡単ではないため、 for の代わりに flatMap を明示的に使用した場合の関数は次のようになります。

def save() = CORSAction { request =>
  def badRequest(message: String) = Error(status = BAD_REQUEST, message)
  
  val updateEither = request.body.asJson.toRight(badRequest("Expecting JSON data"))
    .flatMap { json =>
      json
        .asOpt[Feature]
        .toRight(badRequest("Invalid feature entity"))
    }
    .flatMap { feature =>
       MaxEntitiyValidator
         .checkMaxEntitiesFeature(feature)
         .map(_ => feature)
         .toRight(badRequest("You have already reached the limit"))
     }
     .map { rs =>
       toJson(feature.update).toString
     }

  featureEither match {
    case Right(update) => Ok(update)
    case Left(error)   => BadRequest(toJson(error))
  }
}

パラメータのスコープに関しては、関数がネストされている場合はライブで動作し、チェーンされていないことに注意してください。

結論

適切なフレームワークや適切な言語機能を使用していないことよりも、提供されたコードの主な問題は、エラーがどのように処理されるかだと思います。一般に、メソッドの最後にエラー パスが積み重なったと思い込んでエラー パスを記述しないでください。エラーが発生したときにすぐに対処できる場合は、別の場所に移動できます。それどころか、それらを押し戻すほど、不可解なネストを持つコードが増えます。これらは実際には、ある時点で処理することを scala が期待するすべての保留中のエラー ケースの具現化です。

于 2021-12-07T09:00:29.313 に答える