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
つまり、本質的に、関数のマッピングInt
をInt
1 つのマッピングOption[Int]
に変更しましたOption[Int]
(ただし、構文が少し混乱していますが、"持ち上げる" メタファーは、Haskell のような言語でより明確になります)。
次に、同様の方法でandに次のadd
メソッドを適用するとします。x
y
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
アプリケーション ファンクターに持ち込んでいるだけです。f
3 つの引数を持つメソッドがある場合、次のように記述できます。
val xyz = (x |@| y |@| z)(f)
等々。
では、モナドがあるのに、なぜ Applicative Functor を気にする必要があるのでしょうか? まず第一に、処理したい抽象化のいくつかに対してモナドのインスタンスを提供することは単純に不可能です — <code>Validation は完璧な例です。
2 番目に (そして関連して)、仕事を成し遂げる最も強力でない抽象化を使用することは、堅実な開発プラクティスです。原則として、これにより、他の方法では不可能な最適化が可能になる可能性がありますが、さらに重要なことは、記述したコードがより再利用しやすくなるということです。