2012年5月14日月曜日

Scala Tips / Validation (11) - モナド

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

ここで、Validationを「モナド」と呼んでいますがValidationの場合はちょっと注意が必要なので、その補足記事です。

Scalaのモナド

Scalaの場合、以下のメソッドが文法上特別扱いされています。特別なインタフェースなどを実装しなくても、これらのメソッドさえ定義すればfor式が自動的に解釈するという異例の扱いです。

  • map
  • flatMap
  • filter, withFilter
  • foreach

この中で、for/yield式に使われるのがmapとflatMap、(yieldのない)for式に使われるのがforeachです。filterとwithFilterは両方で使われます。

モナドを「unit演算、bind演算がありモナド則を満たす演算機構を持つオブジェクト」とすると、Scalaの場合は引数一つのコンストラクタがunit演算、flatMapメソッドがbind演算に対応し、これらの演算がモナド則を満たすオブジェクトがモナドということになります。

目安としては、flatMapメソッドが提供されているオブジェクトをモナドとして考えてよいでしょう。(厳密にはモナド則が満たされていることを確認する必要がありますが、これはオブジェクト実装者を信用するということでクリアすることにします。)

for/yield式

for/yield式はモナド演算の文法糖衣ですが、モナド演算に加えてファンクタ演算とフィルタ演算の文法糖衣となっています。

flatMapを提供しているオブジェクトをモナドと呼ぶことにするとすると、同様にmapメソッドが提供されているオブジェクトはファンクタ(関手)と呼ぶことができるでしょう。

フィルタ演算は、モナドやファンクタのような圏論の概念とは直接は関係ないと思いますが、for式で使用することができるようになっています。filterメソッドまたはwithFilterメソッドを提供するオブジェクトはこのフィルタ機能をfor式から使うことができます。なお、withFilterメソッドがfor式向けに最適化されたフィルタで、for式ではwithFilterメソッドがない場合にfilterメソッドを使うようになっています。

for式(yield句なし)

yield句なしのfor式は、手続き型的なループによる制御構造を実現します。副作用を前提とした制御構造なので、圏論的な概念とのつながりはありません。

foreachメソッドを提供したオブジェクトはfor式から使用することができます。

このfor式の場合もフィルタ機能は持っているのでwithFilterメソッドまたはfilterメソッドは有効に機能します。

広義のモナド

Scalaの場合、for式で使えないとモナドとしての利用価値が著しく落ちるので、広い意味では以下の3つのメソッドを提供しているオブジェクトをモナドと考えておくとよいでしょう。もちろん、withFilter/filterが提供されていればより利便性は高くなります。flatMapメソッドを提供しているオブジェクトはたいてい他のメソッドも提供しているので、いずれにしてもflatMapメソッドが目安となります。

  • map
  • flatMap
  • foreach
Validation

Validationは、mapメソッド、flatMapメソッド、foreachメソッドを提供しています。このため、ValidationはScala的にはモナドということになります。

ScalazのMonad

モナドを「unit演算、bind演算がありモナド則を満たす演算機構を持つオブジェクト」とすると、Scalaの場合は演算機構を対象となるオブジェクトのメソッドとして実現していました。一方Scalazの場合は演算機構を(対象となるオブジェクトのメソッドではなく)型クラスのインスタンスとして提供します。

ScalazではMonadという型クラスが提供されています。この型クラスMonadの型クラスインスタンスを持っているオブジェクトが、Scalazにおけるモナドということになります。

ScalazにおけるMonad関連の型クラスの継承関係は以下のようになっています。



型クラスMonadは、Bind&Apply&Functor&Pureとなっていますが、このうちunit演算を型クラスPure、bind演算を型クラスBindが提供しています。

Monadの基本演算であるbind演算は型クラスBindで実現しています。このため型クラスBind&型クラスPureが提供されていれば事実上モナドとして動作することができます。たとえば>>=メソッドを使用することができます。

型クラスMonadは型クラスBind&型クラスPureを引き継いでいるので名実ともにモナドということになります。たとえばfoldLeftMメソッドやreplicateMメソッドなどは型クラスMonadに対応しているので、こういった高度な機能を使う場合は型クラスMonadの型クラスインスタンスが必要になります。

Validation

Scalazの基本設定では、以下の型クラスに対してValidationの型クラスインスタンスが定義されています。

  • Applicative
  • Apply
  • Pointed
  • Functor
  • Pure

一方、MonadとBindの2つの型クラスではValidationの型クラスが定義されていません。(型クラスInvariantFunctorの型クラスインスタンスも提供されていませんが今回の議論では無関係なので無視します。)

このため、型クラスMonadや型クラスBindが対象となる演算に対して、Scalazの基本設定ではValidationは対象外になっています。

つまり、Scalazの中ではValidationはモナドではない、ということです。

整理すると、ValidationはScala的にはモナドである一方、Scalazではモナドではない、ということですね。現象としてはflatMapメソッドは使えfor式にもモナドとして認識されるものの、>>=メソッドやfoldLeftMメソッド、replicateMメソッドなどScalazのモナド向けの機能は使えません。

モナドに関してValidationは、ちょっとイレギュラーな実現方法になっているので注意が必要です。

Validation.Monad

Scalazの基本設定ではValidationはMonadではありませんが、設定をするとMonadとして使用することができます。

具体的には「import Validation.Monad._」のおまじないをすれば、ValidationをScalazのモナドとして使うことができるようになります。

ただし、こうするとValidationをApplicative Functorとして使うときの挙動が変わってくるようです。詳しくは「Scala Tips / Validation (5) - flatMap」のノートを参照してください。

このためValidationをMonadとして使う場合には、以下のように設定をimportするスコープを区切るとよいでしょう。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, List[Int]] = {
  import Validation.Monad._
  a.replicateM[List](10)
}

replicateMメソッドはMonad内の要素を10回複製したものを、新たに作成したコンテナとして動作するMonoidに格納し、このMonoidをさらにMonadに格納して返すメソッドです。

ここでは、ValidationがMonad、ListがMonoidになります。

以下のように動作します。文章にして書くと難しくみえますが、動作はそれほど難しいものではありません。

scala> val x = 1.successNel[Throwable]
x: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(1)

scala> f(x)
res182: scalaz.Scalaz.ValidationNEL[Throwable,List[Int]] = Success(List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1))

このように「import Validation.Monad._」のおまじないをすれば、ValidationをMonadとして使用することができるわけです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿