2012年6月4日月曜日

Scala Tips / Validation (22) - sequence

Validationを操作する場合には、List[A]をList[ValidationNEL[Throwable, B]]を経由してValidationNEL[Throwable, List[B]]に変換する処理が頻出します。

前回は、traverseメソッドを用いてList[A]を直接ValidationNEL[Throwable, List[B]]に変換する処理について説明しました。

Validation (16) - 多重度1の実装」で説明したとおり、Validationを使う場合には「A→ValidationNEL[Throwable,B]」の関数を部品として用意するのが、Scalaプログラミングのコツ、Scalaプログラミング戦略のパターンです。(より汎用的には「A→M[B]」(Mはモナド)です。)

「A→ValidationNEL[Throwable,B]」の関数をList[A]に適用すると、List[ValidationNEL[Throwable, B]]になるので、このList[ValidationNEL[Throwable, B]]はValidationを使うプログラムでは自然に登場してくるデータ構造ということができます。

今回は、このList[ValidationNEL[Throwable, B]]をValidationNEL[Throwable, List[B]]に変換する方法について考えます。

課題

この関数を用いて、以下の関数を作ります。

  • f[T](xs: List[ValidationNEL[Throwable, T]): ValidationNEL[Throwable, List[T]]

動作確認

Validationに入る前に、ListとOptionを使って「M[N[A]]→N[M[A]]」の変換方法について説明します。

まず、traverseメソッドを使う場合には以下のように「x => x」と要素の変換をしない関数を設定すると、と自然にList[Option[Int]]がOption[List[Int]](「M[N[A]]→N[M[A]]」)に変換されます。

scala> List(1.some, 2.some, 3.some).traverse(x => x)
res38: Option[List[Int]] = Some(List(1, 2, 3))

クロージャ「x => x」の代わりにidentity関数を用いることもできます。

scala> List(1.some, 2.some, 3.some).traverse(identity)
res45: Option[List[Int]] = Some(List(1, 2, 3))
sequence

traverseメソッドを使うと「x => x」のクロージャやidentity関数を設定するのが若干冗長です。「M[N[A]]→N[M[A]]」の単純な変換は頻出なので、これを行う専用のメソッドが提供されています。これがsequenceメソッドです。

scala> List(1.some, 2.some, 3.some).sequence
res19: Option[List[Int]] = Some(List(1, 2, 3))

Option要素の中にNoneがある場合には、処理全体を自動的にNoneにしてくれます。こういったMonadicな処理を自動的にしてくれます。

scala> List(1.some, none, 3.some).sequence
res20: Option[List[Int]] = None

validationに適用

前節では、Optionを例にListのsequenceメソッドの一般的な使い方について説明しました。

Validationの場合も、基本的にはOptionの場合と同じなのですが、型パラメータ数が2になる点が異なります。sequenceメソッドは型パラメータ数1のApplicativeを前提としているので型パラメータ数の調整が必要になるわけです。この対処のためには色々な方法がありますが、ここでは新しい型VNTを定義する方法を採用します。

scala> type VNT[A] = ValidationNEL[Throwable, A]
defined type alias VNT

以下のようにList("1", "2", "3")に対して前回使用したvalidate関数をmapメソッドで適用すると「List(Success(1), Success(2), Success(3))」が得られます。考えるのは、この状態のデータをいかにしてSuccess(List(1, 2, 3))に変換するのかという問題です。

scala> List("1", "2", "3").map(validate)
res82: List[scalaz.Scalaz.ValidationNEL[Throwable,Int]] = List(Success(1), Success(2), Success(3))

traverseメソッドを使って変換すると以下のようになります。型パラメータでVNTとIntを指定する点が、Optionの場合と異なります。これはValidationの型パラメータ数が2なのに対して、traverseメソッドは型パラメータ数1を想定しているためで、その調整のために型パラメータを指定する必要があります。

scala> List(Success(1), Success(2), Success(3)).traverse[VNT, Int](x => x)
res46: VNT[List[Int]] = Success(List(1, 2, 3))

identity関数を使うと以下のようになります。

scala> List(Success(1), Success(2), Success(3)).traverse[VNT, Int](identity)
res47: VNT[List[Int]] = Success(List(1, 2, 3))
sequence

sequenceメソッドを使うと以下のようになります。型パラメータの指定は必要ですが、クロージャ「x -> x」やidentity関数の指定は必要ありません。

scala> List(Success(1), Success(2), Success(3)).sequence[VNT, Int]
res48: VNT[List[Int]] = Success(List(1, 2, 3))

要素にFailureが入っている場合は、自動的に変換結果はFailureになります。

scala> List(Success(1), new IllegalArgumentException("bad").failNel, Success(3)).sequence[VNT, Int]
res51: VNT[List[Int]] = Failure(NonEmptyList(java.lang.IllegalArgumentException: bad))

結果

以上の結果を課題の関数にまとめたものが以下になります。

def f(a: List[ValidationNEL[Throwable, Int]]): ValidationNEL[Throwable, List[Int]] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  a.sequence[VNT, Int]
}

末端の要素になるInt型は、処理自体に処理を与えないので型パラメータTにして汎用化すると以下になります。

def f[T](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, List[T]] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  a.sequence[VNT, T]
}

ノート

前回「Validation (21) - traverse」で取り上げたtraverseメソッドとsequenceメソッドは、Monadicプログラミングで頻出の処理を簡単にさばいてくれるので、Monadicプログラミングをする以上必須のメソッドと思います。

おさらいすると、traverseメソッドは:

  • List[A]をValidationNEL[Throwable, List[B]]に変換
  • より汎用的には、M[A]をN[M[B]]に変換 (NはApplicative, MはTraverse)

します。sequenceメソッドは、List[A]をList[ValidationNEL[Throwable, B]]に変換したものがすでにある場合:

  • List[ValidationNEL[Throwable, B]]をValidationNEL[Throwable, List[B]]に変換
  • より汎用的には、M[N[B]]をN[M[B]]に変換 (NはApplicative, MはTraverse)

します。

fold系のメソッドを使って毎回スクラッチで書くこともできないはありませんが、事前に準備されている汎用部品を使う方が効率がよいのは言うまでもありません。

プログラミング戦略的には、最後にtraverseメソッドやsequenceメソッドがある前提で、ロジックを組み立てることができるのが非常に大きいです。Monadicプログラミングでは、「A→M[B]」の形の関数を軸にロジックを組み立てていきますが、ここからM[N[B]]をN[M[B]]にする処理が頻出することになります。この処理の捌き方が定型的に定まっていることがプログラミングの負担を大幅に軽減します。

Scalazを使用するメリットは多数ありますが、Scala本体に同等品がないということも含めて、実用的な観点でtraverseメソッドとsequenceメソッドの存在も非常に大きいと思います。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿