7

条件のリストを渡す場合、オブジェクトを永続化するメソッドを開発しています。

いずれか (または多く) の条件が失敗した場合 (またはその他の種類のエラーが表示された場合)、エラーのリストが返されます。すべてがうまくいけば、保存されたエンティティが返されます。

私は次のようなことを考えていました(もちろん疑似コードです):

request.body.asJson.map { json =>
  json.asOpt[Wine].map { wine =>
    wine.save.map { wine => 
      Ok(toJson(wine.update).toString)
    }.getOrElse  { errors => BadRequest(toJson(errors))}
  }.getOrElse    { BadRequest(toJson(Error("Invalid Wine entity")))}
}.getOrElse      { BadRequest(toJson(Error("Expecting JSON data")))}

つまり、それを Option[T] のように扱いたいと思います。検証が失敗した場合、Noneそれを返す代わりに、エラーのリストが返されます...

アイデアは、JSON エラーの配列を返すことです...

質問は、このような状況を処理する正しい方法ですか? そして、Scala でそれを達成する方法は何でしょうか?

--

おっと、ちょうど質問を投稿して、どちらかを発見しました

http://www.scala-lang.org/api/current/scala/Either.html

とにかく、選択したアプローチについてどう思うか、それを処理するための他のより良い代替手段があるかどうかを知りたい.

4

4 に答える 4

13

scalazを使用すると、のValidation[E, A]ようEither[E, A]になりますが、ifEが半群(リストのように連結できるものを意味します)である場合、複数の検証済み結果を組み合わせて、発生したすべてのエラーを保持できるという特性があります。

たとえば、Scala2.10-M6とScalaz7.0.0-M2を使用します。ここで、Scalazには、デフォルトで右バイアスされたEither[L, R]名前のカスタムがあります。\/[L, R]

import scalaz._, Scalaz._

implicit class EitherPimp[E, A](val e: E \/ A) extends AnyVal {
  def vnel: ValidationNEL[E, A] = e.validation.toValidationNEL
}

def parseInt(userInput: String): Throwable \/ Int = ???
def fetchTemperature: Throwable \/ Int = ???
def fetchTweets(count: Int): Throwable \/ List[String] = ???

val res = (fetchTemperature.vnel |@| fetchTweets(5).vnel) { case (temp, tweets) =>
  s"In $temp degrees people tweet ${tweets.size}"
}

これresultは、Validation[NonEmptyList[Throwable], String]発生したすべてのエラー(温度センサーエラーおよび/またはTwitterエラーまたはなし)または成功したメッセージのいずれかを含むです。\/その後、便宜上に戻ることができます。

注:EitherとValidationの違いは、主に、Validationを使用するとエラーを蓄積できますがflatMap、蓄積されたエラーを失うことはできませんが、Eitherを使用すると(簡単に)蓄積することはできませんがflatMap(または理解のために)失う可能性があります最初のエラーメッセージを除くすべて。

エラー階層について

これはあなたにとって興味深いかもしれないと思います。scalaz / Either/ \//を使用するかどうかに関係なく、Validation開始は簡単でしたが、今後は追加の作業が必要になることを経験しました。問題は、複数の誤った関数から意味のある方法でエラーをどのように収集するかということです。確かに、どこでも、ThrowableまたはList[String]どこでも使用でき、簡単な時間を過ごすことができますが、あまり使いやすく、解釈しやすいようには聞こえません。「子供の年齢がありません」::「IOエラー読み取りファイル」::「ゼロ除算」などのエラーのリストを取得することを想像してみてください。

したがって、私の選択は、Javaのチェックされた例外を階層にラップするのと同じように、(ADTを使用して)エラー階層を作成することです。例えば:

object errors {

  object gamestart {
    sealed trait Error
    case class ResourceError(e: errors.resource.Error) extends Error
    case class WordSourceError(e: errors.wordsource.Error) extends Error
  }

  object resource {
    case class Error(e: GdxRuntimeException)
  }

  object wordsource {
    case class Error(e: /*Ugly*/ Any)
  }

}

次に、エラータイプが異なる関数のエラーの結果を使用する場合、関連する親エラータイプの下でそれらを結合します。

for {
  wordSource <-
    errors.gamestart.WordSourceError <-:
    errors.wordsource.Error <-:
    wordSourceCreator.doCreateWordSource(mtRandom).catchLeft.unsafePerformIO.toEither

  resources <-
    errors.gamestart.ResourceError <-:
    GameViewResources(layout)

} yield ...

ここでは、Bifunctorであるため、左側のf <-: e関数をマップします。あなたのために持っているかもしれません。fe: \/\/se: scala.Eitherse.left.map(f)

これは、シェイプレス HListIsoを提供して適切なエラーツリーを描画できるようにすることでさらに改善される可能性があります。

改訂

更新:(e: \/).vnel障害側をに引き上げ、障害が発生したNonEmptyList場合、少なくとも1つのエラーが発生します(以前は:またはなし)。

于 2012-08-22T09:04:48.597 に答える
4

Option値があり、それらを成功/失敗の値に変換したい場合は、orメソッドを使用して をOptionに変換できます。EithertoLefttoRight

通常、 aRightは成功を表すため、とに変換するために使用o.toRight("error message")します。Some(value)Right(value)NoneLeft("error message")

残念ながら、Scala はデフォルトでこの右バイアスを認識しないため、for 内包表記で s.rightをきちんと構成するには、(メソッドを呼び出して)フープをジャンプする必要があります。Either

def requestBodyAsJson: Option[String] = Some("""{"foo":"bar"}""")

def jsonToWine(json: String): Option[Wine] = sys.error("TODO")

val wineOrError: Either[String, Wine] = for {
  body <- requestBodyAsJson.toRight("Expecting JSON Data").right
  wine <- jsonToWine(body).toRight("Invalid Wine entity").right
} yield wine
于 2012-08-22T09:15:07.183 に答える
2

空の値が必要な場合は、を使用する代わりに、次の3つの値を持つEither[A,Option[B]]liftを使用できます。Box

  • Full(有効な結果があります)
  • Empty(結果はありませんが、エラーもありません)
  • Failure(エラーが発生しました)

Box豊富なAPIEitherのおかげよりも柔軟性があります。もちろん、Lift用に作成されていますが、他のフレームワークで使用できます。

于 2012-08-22T08:02:32.533 に答える
0

まあ、これはどちらかを使った私の試みです

def save() = CORSAction { request =>
  request.body.asJson.map { json =>
    json.asOpt[Wine].map { wine =>
      wine.save.fold(
        errors => JsonBadRequest(errors),
        wine => Ok(toJson(wine).toString)
      )
    }.getOrElse     (JsonBadRequest("Invalid Wine entity"))
  }.getOrElse       (JsonBadRequest("Expecting JSON data"))
}

そして、wine.save は次のようになります。

def save(wine: Wine): Either[List[Error],Wine] = {

  val errors = validate(wine)
  if (errors.length > 0) {
    Left(errors)
  } else {
    DB.withConnection { implicit connection =>
      val newId = SQL("""
        insert into wine (
          name, year, grapes, country, region, description, picture
        ) values (
          {name}, {year}, {grapes}, {country}, {region}, {description}, {picture}
        )"""
      ).on(
        'name -> wine.name, 'year -> wine.year, 'grapes -> wine.grapes,
        'country -> wine.country, 'region -> wine.region, 'description -> wine.description,
        'picture -> wine.picture
      ).executeInsert()

      val newWine = for {
        id <- newId;
        wine <- findById(id)
      } yield wine

      newWine.map { wine =>
        Right(wine)
      }.getOrElse {
        Left(List(ValidationError("Could not create wine")))
      }
    }
  }
}

Validate はいくつかの前提条件をチェックします。データベースエラーをキャッチするには、まだtry/catchを追加する必要があります

私はまだ全体を改善する方法を探しています。私の好みでは冗長に感じます...

于 2012-08-23T06:45:47.933 に答える