ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。Validationを使ってOptionと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。
前回はmapメソッドを用いて行う成功/失敗の文脈を切り替えない演算、flatMapメソッドを用いて行う成功/失敗の文脈を切り替える演算をfor式で記述する方法について説明しました。
for式も悪くないのですが、簡単な演算だとmapメソッドやflatMapメソッドを使う方がより簡潔な記述になりました。
今回は、もう少し複雑な演算を考えてみます。
課題
以下に示す3つの関数mul101、toText、toLabelを組合わせてValidationNEL[Throwable, Int]をValidationNEL[Throwable, String]に変換する演算を考えます。
def mul101(a: Int): ValidationNEL[Throwable, Int] = { if (a >= 0) (a * 101).success else new IllegalArgumentException("less than 0: " + a).failNel } def toText(a: Int): ValidationNEL[Throwable, String] = { if (a % 2 == 0) a.toString.success else new IllegalArgumentException("not even: " + a).failNel } def toLabel(a: String): ValidationNEL[Throwable, String] = { if (a.length < 5) ("Success:" + a.toString).success else new IllegalArgumentException("large length: " + a).failNel }
- mul101
- Intを101倍する。0未満の場合はエラー。
- toText
- IntをStringにする。奇数の場合はエラー。
- toLabel
- Stringを整形する。文字数が5以上の場合はエラー。
いずれの関数も、入力パラメタの値によってエラーになるところがポイントです。これらの関数を組合わせて、成功の文脈と失敗の文脈を切り替える処理を、正常系のアルゴリズム(成功の文脈)を簡潔に分かりやすく記述することを目指します。
(分類の基準)
Java風
キャストが多くなってしまうのはValidationにSuccessやFailureの値を直接取ってこれる機能がないのが原因ですが、かなり込み入ったコーディングになります。前回の簡単な処理ぐらいであれば許容範囲ですが、ちょっとロジックが複雑になると耐えられないコーディングになってしまいます。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { if (a.isSuccess) { val b = a.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a val c = mul101(b) if (c.isSuccess) { val d = c.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a val e = toText(d) if (e.isSuccess) { val f = e.asInstanceOf[Success[NonEmptyList[Throwable], String]].a val g = toLabel(f) if (g.isSuccess) { val h = g.asInstanceOf[Success[NonEmptyList[Throwable], String]].a Success(h) } else { g.asInstanceOf[ValidationNEL[Throwable, String]] } } else { e.asInstanceOf[ValidationNEL[Throwable, String]] } } else { c.asInstanceOf[ValidationNEL[Throwable, String]] } } else { a.asInstanceOf[ValidationNEL[Throwable, String]] } }
Scala風
match式を使うとそれなりのコーディングにはなりますが、かなり面倒なコーディングであることは変わりません。ネストが込み入っているので全体の見通しが悪くなりますし、ボイラープレート的なコードでメインロジックが埋もれてしまっています。エラーハンドリングのコードが正常処理と同じ重み付けで全体の半分を閉めてしまうのも感心しないところです。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { a match { case Success(b) => { mul101(b) match { case Success(c) => { toText(c) match { case Success(d) => { toLabel(d) match { case Success(e) => Success(e) case Failure(g) => Failure(g) } } case Failure(h) => Failure(h) } } case Failure(i) => Failure(i) } } case Failure(j) => Failure(j) } }
このコードはパターンマッチングは使っていますが、手続き型(OOP含む)の典型的なコーディングになります。
これではたまらないので、エラー処理に例外機構を用いて、正常系処理のコードを簡潔に保つのがOOPで一般に用いられている方法です。ただし、その場合でもシステムエラーもアプリケーションエラーも一律にエラー終了にするといったエラー処理はよいのですが、アプリケーションエラーをアプリケーションロジックで扱うといった用途とは相性が悪いので、コーディング上の工夫が必要になってきます。
Scala
ScalaではflatMapを用いて処理を簡潔に記述するのが普通のコーディングスタイルです。これはValidationがモナドであるために可能になっています。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { a.flatMap(mul101).flatMap(toText).flatMap(toLabel) }
Scalaz
Scalaz的に>>=メソッドを使うと以下のようになります。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { import Validation.Monad._ a >>= mul101 >>= toText >>= toLabel }
for式
さて、本題のfor式で書くと以下のようになります。「Scala」、「Scalaz」と比べると若干冗長な気もしますが、「Java風」、「Scala風」と比べると比較にならないほど簡潔に記述することができます。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { for { b <- a c <- mul101(b) d <- toText(c) e <- toLabel(d) } yield e }
より普通のfor式っぽく以下のように書くこともできます。
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = { for (b <- a; c <- mul101(b); d <- toText(c); e <- toLabel(d)) yield { e } }
ノート
for式で簡潔に書ける例を考えてみたのですが、結局flatMapの方がもっと簡潔に書けてしまいました。Monadicプログラミングに慣れてくると、mapやflatMapを好むようになりますが、この辺の事情によりますね。
ただ、for式が「Java風」や「Scala風」よりも圧倒的に簡潔なのは確かなので、flatMapでの実現方法が思いつかない場合でも、for式を使う方向で考えていくとよいでしょう。
今回のように演算を単純につないでいく用途ではflatMapの方が便利ですが、そうでない場合はfor式の方が便利なケースがあります。次回はそのケースを取り上げたいと思います。
追記 (2011-04-26)
>>=メソッドを使うのに「 import Validation.Monad._」が抜けていたので追加しました。諸元
- Scala 2.9.2
- Scalaz 6.0.4
0 件のコメント:
コメントを投稿