2012年6月21日木曜日

Scala Tips / Validation (33) - reduce

fold系メソッドのバリエーションとしてreduce系のメソッドがあります。

fold系メソッドは、畳込みの初期値を指定するので元のオブジェクトの集りを全く異なったオブジェクトに変換することができます。

一方、reduce系メソッドは畳込みの初期値を指定しません。引数の数が減るというメリットがありますが、その代償として、変換先が元のオブジェクトまたはその親クラスのオブジェクトに限定されます。

初期値の設定が不要なので少し使い方が簡単になっている面があります。このため「変換先が元のオブジェクトまたはその親クラスのオブジェクトに限定」されてもよいケースでは有力な選択肢です。たとえばIntの集りをIntに畳み込む場合は初期値はなくても構わないのでreduce系メソッドでも十分なわけです。

今回はValidationに対してreduce系メソッドを適用するイディオムについて見ていきます。

課題

Int型の値を格納したValidationのリスト上で、Int型の値を加算で畳み込んで結果の値を格納したValidationを生成します。

具体的には、以下の関数を作ります。

  • f(a: List[ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, Int]

この課題は「Validation (23) - fold」と同じものです。

reduce, reduceLeft, reduceRight

foldメソッド、foldLeftメソッド、foldRightメソッドに対応するのがreduceメソッド、reduceLeftメソッド、reduceRightメソッドです。Applicativeを使った実装は、それぞれ以下のようになります。

def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  if (a.isEmpty) 0.success
  else a.reduce((a, b) => (a |@| b)(_ + _))
}
def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  if (a.isEmpty) 0.success
  else a.reduceLeft((a, x) => (a |@| x)(_ + _))
}
def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  if (a.isEmpty) 0.success
  else a.reduceRight((x, a) => (a |@| x)(_ + _))
}

reduceメソッド、reduceLeftメソッド、reduceRightメソッドの選択基準はfoldの場合と同じで、右畳込みであるreduceRightを軸にするとよいでしょう。

reduceメソッド、reduceLeftメソッド、reduceRightメソッドの使用上の問題点は、ListがNilだった時に例外が発生する点です。このため使用する前にListが空の場合にはデフォルトの値を使うようにするなどの対応が必要になります。

reduceOption, reduceLeftOption, reduceRightOption

空の場合の扱いを考えるとListが空か否かをOptionで扱うのが常道です。このためreduceのOption版であるreduceOptionメソッド、reduceLeftOptionメソッド、reduceRightOptionメソッドが用意されています。reduceOptionメソッド、reduceLeftOptionメソッド、reduceRightOptionメソッドを使った実装はそれぞれ以下になります。

def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  a.reduceOption((a, b) => (a |@| b)(_ + _)) | 0.success
}
def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  a.reduceLeftOption((a, x) => (a |@| x)(_ + _)) | 0.success
}
def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  a.reduceRightOption((x, a) => (a |@| x)(_ + _)) | 0.success
}

Optionからデータを取り出し、Noneの場合にはデフォルト値を補う処理が必要になります。

sequence

ValidationのListを扱う場合はsequenceメソッドを使うのがイディオムになっています。sequenceメソッドを使った実装は以下になります。

def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, Int] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  a.sequence[VNT, Int].map(_.reduceRightOption((x, a) => x + a) | 0)
}

reduce系の場合もこのアプローチが当てはまります。

monoid

fold系の場合と同様に、Validationが保持する値をIntのような具象オブジェクトではなく、Monoidを対象にすると汎用性が高まります。この実装は以下になります。

def f[T: Monoid](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, T] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  a.sequence[VNT, T].map(_.reduceRightOption((x, a) => x |+| a) | mzero[T])
}

ノート

reduce系のメソッドは初期値を指定しなくて良いので一見便利そうなのですが、Listが空だった時の判定を行わないといけないので案外使いづらいようです。プログラミング戦略としては、reduce系のメソッドのことはあまり考えず、fold系一本で考えてよいと思います。

reduce系のメソッドは、対象をMonoid(あるいは型クラスZero)に限定することで、初期値の省略時にはクラスごとのデフォルトの初期値を使うようになっていると便利そうです。そういう意味では、Scalazのsum/sumrメソッド、foldMapメソッドがまさにそういった便利メソッドなので、これらのメソッドが重要な選択肢になってきます。

Validationのreduce的な処理が必要なときは、sequenceで加工した後に:

  • sum/sumrメソッド
  • foldMapメソッド
  • fold系メソッド

の順に使用するfold由来メソッドを検討していけばよいでしょう。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿