2012年4月26日木曜日

Scala Tips / Validation (9) - forで演算を連結2

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。Validationを使ってOptionやEitherと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

前々回はmapメソッドを用いて行う成功/失敗の文脈を切り替えない演算、flatMapメソッドを用いて行う成功/失敗の文脈を切り替える演算をfor式で記述する方法について説明しました。また、前回はより複雑な例をflatMapメソッドとfor式で記述しました。

いずれの場合も、flatMapメソッドの方がfor式よりも簡潔に記述できました。

今回は、さらに問題を複雑にしてfor式の方が簡潔になるケースについて考えます。

課題

以下に示す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以上の場合はエラー。

いずれの関数も、入力パラメタの値によってエラーになるところがポイントです。これらの関数を組合わせて、成功の文脈と失敗の文脈を切り替える処理行うわけですが、正常系のアルゴリズム(成功の文脈)を簡潔に分かりやすく記述することを目指します。

ここまでは前回と同じです。

今回は、これらの演算を組み合わせた上で、mul101関数の結果、toText関数の結果、toLabel関数の結果を一つの文字列にまとめる処理を行います。

(分類の基準)

Java風

中核のロジックは「"%s -> %s -> %s -> %s".format(b, d, f, h)」です。ボイラープレートのコードに埋もれてしまい、前回との違いがよく分かりません。

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("%s -> %s -> %s -> %s".format(b, d, f, 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("%s -> %s -> %s -> %s".format(b, c, d, e))
                case Failure(g) => Failure(g)
              }
            }
            case Failure(h) => Failure(h)
          }
        }
        case Failure(i) => Failure(i)
      }
    }
    case Failure(j) => Failure(j)
  }
}

Scala

flatMapメソッドを用いて書くと以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap(b => mul101(b).flatMap(c => toText(c).flatMap(d => toLabel(d).map(e =>
    "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}

前回との違いは、 中核ロジックである「"%s -> %s -> %s -> %s".format(b, c, d, e)」が、最初の入力、mul101関数の結果、toText関数の結果、toLabel関数の結果を使用することです。前回は最後に実行するtoLabel関数の結果のみを使っていました。

このため、最初の入力、mul101関数の結果、toText関数の結果、toLabel関数の結果を変数に覚えておき、後ろの段で参照する必要があります。これを実現するためには、各関数の結果を変数に設定するようにすると同時に、flatMapメソッド内で実行する関数の中で次のflapMapメソッドを呼ぶというように、flatMapメソッドをネストして実行します。

flatMapの実行結果のみを次のflatMapに渡すだけであれば、前回のようにパイプライン的につないでいけばよいのですが、前段の処理結果を後段で参照する必要がある場合は、flatMapをネストさせていかなければならないわけです。変数を設定する処理、ネストしていること、を一行にまとめてしまうと可読性が悪いので、これを整形すると以下のようになります。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap(b =>
    mul101(b).flatMap(c =>
      toText(c).flatMap(d =>
        toLabel(d).map(e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}

また、以下のようにも書けます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap { b =>
    mul101(b).flatMap { c =>
      toText(c).flatMap { d =>
        toLabel(d).map { e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)
        }
      }
    }
  }
}

このようになってくるとflatMapメソッドであっても、簡潔に記述という感じではなくなってきました。

Scalaz

Scalaz的に>>=メソッドを使うと以下のようになります。Validationに対して>>=メソッドを使う場合は「import Validation.Monad._」のおまじないをしないといけないのが難点です。(「Validation (5) - flatMap」参照)

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= (b => mul101(b) >>= (c => toText(c) >>= (d => toLabel(d).map(e =>
    "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= (b =>
    mul101(b) >>= (c =>
      toText(c) >>= (d =>
        toLabel(d).map(e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)))))
}
def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  import Validation.Monad._

  a >>= { b =>
    mul101(b) >>= { c =>
      toText(c) >>= { d =>
        toLabel(d).map {e =>
          "%s -> %s -> %s -> %s".format(b, c, d, e)
        }
      }
    }
  }
}

for式

さて、本題のfor式で書くと以下のようになります。for式は前回の場合と複雑度はほとんど変わりません。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for {
    b <- a
    c <- mul101(b)
    d <- toText(c)
    e <- toLabel(d)
  } yield "%s -> %s -> %s -> %s".format(b, c, d, e)
}

より普通のfor式っぽく以下のように書くこともできます。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  for (b <- a; c <- mul101(b); d <- toText(c); e <- toLabel(d)) yield {
    "%s -> %s -> %s -> %s".format(b, c, d, e)
  }
}

ノート

flatMapで関数を接続する演算で、前段で実行した各関数の結果を直後ではない後段で参照するようなケースでは、(1)関数の実行結果を覚えておく変数(クロージャの引数)の導入、(2)flatMapをネストで実行、を行う必要があるため、プログラムが複雑化します。

一方、for式はこのような使い方でも使い勝手は全く一緒です。

このため、今回の課題ではfor式の方が簡潔に記述することができます。

Monadicプログラミングをしていくと、flatMapを多段につないでいくことになりますが、処理が複雑化していくと今回の課題のようなネスト構造が登場して、プログラムの取り回しに苦労するようになります。これがMonadicプログラミングで頻出のパターンになので、これに対する文法糖衣としてfor式が用意されているわけですね。

簡単なパイプライン的な処理の場合はflatMapや>>=を使い、複雑になりそうな場合はfor式を選択するという形で、適材適所で使い分けをしていきたいところです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿