2012年4月20日金曜日

Scala Tips / Validation (5) - flatMap

ValidationはScalazが提供する成功/失敗の計算文脈を提供するモナドです。

Validationを使ってOptionと同様の成功/失敗の計算文脈上でのMonadicプログラミングをすることができます。

今回は以下の処理、アプリケーションの意図で成功の文脈を失敗の文脈に切り替えるためのMonadic演算についてみていきます。「Option(6)」や「Either(2)」で扱った課題のValidation版です。

条件結果
Validation[A, B]がSuccess[A, B]でBに有効な値が入っているSuccess[A, C]
Validation[A, B]がSuccess[A, B]でBに無効な値が入っているFailure[A, C]
Validation[A, B]がFailure[A, B]Failure[A, C]

大枠ではB→Cの演算を行いたいわけですが、これをValidation[A, B→C]の文脈の上で行うわけです。この時、Validation[A, B]がSuccess[A, B]であっても「B」が無効な値である場合には、Failure[A, C]にすることで、成功の文脈から失敗の文脈へ切り替えます。

以下ではValidation[NonEmptyList[Throwable], Int]をValidation[NonEmptyList[Throwable], String]に変換するプログラムを考えます。なお、Validation[NonEmptyList[Throwable], Int]はValidationNEL[Throwable, Int]と同等なので、可能な場合はValidationNELの方の表記を用います。ただし、Intは0以上のものが有効という条件を追加します。Validation(Success)に入っているIntが0以上の場合、Success[NonEmptyList[Throwable], String]が処理結果となります。一方、0未満の場合は無効となりFailure[NonEmptyList[Throwable], String]が処理結果となります。

(分類の基準)

Java風

if式でValidation#isSuccessメソッドを使ってSuccessとFailureの判定をして、処理を切り分ける事ができます。Successの場合に、値の有効判定を行って、無効(0未満)だった場合はFailureを返します。Success、Failure共キャスト(asInstanceOf)を使うことになるので避けたい用法です。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  if (a.isSuccess) {
    val s = a.asInstanceOf[Success[NonEmptyList[Throwable], Int]]
    if (s.a >= 0) {
      Success(s.a.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s.a)))
    }
  } else {
    a.asInstanceOf[ValidationNEL[Throwable, String]]
  }
}

Scala風

match式を使うとSuccessとFailureのパターンマッチングで綺麗に書くことができます。Successの場合に、値の有効判定を行って、無効(0未満)だった場合はFailureを返します。ただし、Scala的には「Failure(b)の場合はFailure(b)」というロジックを書くのが悔しいところです。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a match {
    case Success(s) => if (s >= 0) {
      Success(s.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s)))
    }
    case Failure(e) => Failure(e)
  }
}

Scala

Option」や「Either」と同様に成功の文脈から失敗の文脈の切替えを行うにはflatMapメソッドを使用します。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a.flatMap { s =>
    if (s >= 0) {
      Success(s.toString)
    } else {
      Failure(NonEmptyList(new IllegalArgumentException("value: " + s)))
    }
  }
}

失敗の文脈であるFailureの場合は自動的に引き継がれるのがミソです。

Scalaz

Scalazらしい書き方として、flatMapメソッドと同等の機能を持つ>>=メソッドやその別名の「∗」(Unicode 2217)を使うことができます。

import Validation.Monad._

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a >>= { s =>
    if (s >= 0) s.toString.success
    else new IllegalArgumentException("value: " + s).failNel
  }
}
import Validation.Monad._

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, String] = {
  a ∗ { s =>
    if (s >= 0) s.toString.success
    else new IllegalArgumentException("value: " + s).failNel
  }
}

だたし、いずれも「Validation.Monad._」のインポートが必要となるため、無理をして使う必要はないでしょう。

ノート

Validationで>>=メソッドを使うために「Validation.Monad._」のインポートをすると、Validationに対するApplicativeな動作でFailureのMonoidが積算されないという副作用がでてきます。このメカニズムは、以下のページに詳しい解説がありました。

Validationを使う効用の一つがApplicativeな処理でFailureのMonoidが積算されるという点にあるので、このメリットを放棄するのは得策ではありませんし、デフォルトの動作を暗黙的な形で変えてしまうと原因不明のバグが起きやすくなってしまうので、「Validation.Monad._」のインポートを使う用法はできるだけ避けるのがよいでしょう。

推測ですが、「Validation.Monad._」をめぐる仕様は、Validationに対するApplicativeな動作でFailureの値を:

  • 複数のFailureがある場合に、Monoidとして積算する
  • 複数のFailureがある場合に、そのうちの一つを使う

という仕様上の選択を苦慮した結果かもしれません。

現実装では、Validationの>>=メソッドに対して選択される型クラスBindのValidation用インスタンスは前者の振舞いをします。一方、型クラスMonoidのValidation用インスタンスは後者の振舞いをします。

型クラスインスタンスの解決では、型クラスMonoidの方が型クラスBindより優先度が高いので、この優先度の差を利用して、デフォルト仕様を型クラスBind、カスタマイズ仕様を型クラスMonadで実現してみたのではないかと推測します。

このあたりは、Scalaz 7で導入されるというTagged Typeで解決されるのかな。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿