2012年6月1日金曜日

Scala Tips / Validation (21) - traverse

Validation (18) - 多重度0以上の部品」や「Validation (19) - 多重度1以上の部品」などで分かるように、Validationを操作する場合には、List[A]をList[ValidationNEL[Throwable, B]]を経由してValidationNEL[Throwable, List[B]]に変換する処理が頻出します。

Listなどのシーケンスに格納されている値をすべて検証後、アプリケーションが操作する型に変換して、すべての値の検証がパスした場合には、検証結果をシーケンスに入れて返してもらうという処理です。

この処理には、型クラスTraverseが提供するtraverseメソッドが非常に便利です。

今回はValidationの操作にtraverseメソッドを使用する方法についてみていきます。

課題

検証&値変換を行うvalidation関数があるとします。

def validate(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  allCatch either {
    a.toInt
  } flatMap {
    x => if (x >= 0) x.right
         else new IllegalArgumentException("less 0: " + x).left
  } fold (_.failNel, _.success)
}

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

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

traverseの動作確認

Validationの応用に入る前に、traverseメソッドの基本的な使い方を確認しましょう。

traverseメソッドは、型クラスTraverseに対応するオブジェクト(型クラスTraverseの型クラスインスタンスを定義しているオブジェクト、以下Traverseオブジェクト)に付与されるメソッドで、指定された関数が生成する型クラスApplicativeに対応するオブジェクト(以下Applicativeオブジェクト)の中にTraverseオブジェクトをくるみます。

TraverseオブジェクトをM、ApplicativeオブジェクトをNとすると、M[A]に対してA→N[B]の関数を適用してN[M[B]]に変換する処理を行います。

この処理は以下の2つの処理を同時に行なっています。

  • A→B
  • M[_]→N[M[_]]

前者の「A→B」は普通のmapメソッドと同じです。

後者の「M[_]→N[M[_]]」がtraverseメソッドの肝で、「M[_]→M[N[_]]→N[M[_]]」というように、一度M[N[_]]の入れ子を作り、これを逆転させると考えると分かりやすいと思います。一般的に、TraverseオブジェクトもApplicativeオブジェクトもモナドであることが多いので、分かりやすく単純化した言い方をすると、2つのモナドの入れ子関係を逆転する処理を行います。

まとめるとtraverseメソッドは要素を写像しながら、2つのモナド(コンテナ)の入れ子関係を逆転する処理ということができます。

実際の動作を試してみましょう。

「List(1, 2, 3)」に対して「_.some」すなわち「x => Some(x)」を適用すると以下のように「Some(List(1, 2, 3))」が得られます。この場合、ListがTraverseオブジェクト、OptionがApplicativeオブジェクトです。

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

単純にmapしただけだと以下のように「List(Some(1), Some(2), Some(3))」が得られます。traverseメソッドの場合は、同時にこのListとSomeをひっくり返すわけですね。

scala> List(1, 2, 3).map(_.some)
res39: List[Option[Int]] = List(Some(1), Some(2), Some(3))
モナディックな自動処理

traverseメソッドでは、モナドをひっくり返す際にモナディックな処理を行います。以下の例では、traverseメソッドに「0以上ならSome(x)、0未満ならNoneを返す」関数を指定しており、結果としてNoneが得られています。

scala> List(1, -1, 3).traverse(x => (x >= 0).option(x))
res40: Option[List[Int]] = None

この処理は、以下のようにmapメソッドの動作を考えると分かりやすいでしょう。

scala> List(1, -1, 3).map(x => (x >= 0).option(x))
res41: List[Option[Int]] = List(Some(1), None, Some(3))

「0以上ならSome(x)、0未満ならNoneを返す」関数をmapメソッドに適用すると、「List(Some(1), None, Some(3))」が得られます。

traverseメソッドでは、この結果からさらにモナドの入れ子を逆転させるわけですが、List内のOptionが一つでもNoneの場合は、入れ子を逆転させた結果がNoneになるわけです。

このように、一つでもNoneがあった場合は全体結果もNoneになるというのが、Optionのモナディックな振舞いです。この処理が自動的に行われるのが、Monadicプログラミングのキモとなるところです。定型処理はモナドの裏側で自動的に行うことで、やりたい処理のみの記述に専念できるわけですね。

traverseメソッドの処理も、このモナディックな振舞いによって定型的な処理が自動化されており、とても便利に使えます。traverseメソッドでは、適用する関数「A→N[B]」のNがApplicativeオブジェクトであることがモナディックな振舞いを担保しています。

Validationに適用

Validationは型パラメータ数が2であるため、型パラメータ数1の型パラメータを前提としているtraverseメソッドは、そのままの形では使えません。このギャップを解消するために、VNTという新しい型を追加します。

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

その上で、traverseメソッドに型パラメータとしてVNTとIntを指定して実行してみましょう。

以下のように無事、期待通りの処理が行われました。

scala> List("1", "2", "3").traverse[VNT, Int](validate)
res17: VNT[List[Int]] = Success(List(1, 2, 3))

エラーがあるデータに適用した場合は、Failureとなりエラー情報が記録されています。

scala> List("1", "a", "3").traverse[VNT, Int](validate)
res44: VNT[List[Int]] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "a"))

結果

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

def f(xs: List[String]): ValidationNEL[Throwable, List[Int]] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  xs.traverse[VNT, Int](validate)
}

ノート

Validationを使う場合の型パラメータについての話題です。

Validationは型パラメータ数が2つあるため、traverseメソッドを使う場合には注意が必要です。traverseメソッドは型パラメータ1のApplicativeを生成する関数を指定する必要があります。ValidationはApplicativeですが、型パラメータ数が2つであるために調整が必要になるわけです。

この調整は、Validationの型パラメータ数2のうちの1つを固定して、型パラメータ1の新しい型を定義してこれをtraverseメソッドに指定することで行います。

def f[T](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, List[T]] = {
  a.traverse[({type L[A] = ValidationNEL[Throwable, A]})#L, T](x => x)
}

よく使われているのは、LやAといったアルファベットの代わりにλやαといったギリシャ文字を使う方法です。ギリシャ文字を使うと何か難しいそうなことをやっているような感触を受けてしまいますが、この場合は普通のアルファベットを使うのと全く同じです。

def f[T](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, List[T]] = {
  a.traverse[({type λ[α] = ValidationNEL[Throwable, α]})#λ, T](x => x)
}

型パラメータの引数名としてTやA, Bは、クラスやメソッドの定義でよく使うので、メソッドの実装などで内部的に型を定義する場合にはλやαといったギリシャ文字を使うようにしておくと、名前の衝突が回避できます。これも一種のイディオムといえると思います。

VNT

「({type λ[α] = ValidationNEL[Throwable, α]})#λ」という型は、便利ですがプログラムの可読性が落ちるので、分りやすさを取る場合は新しい型を定義します。以下ではVNTという名前で型を定義しました。

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

先ほど説明したように、ギリシャ文字を使うことで名前の衝突を回避することができます。その場合には以下のようになります。

def validates[T[_]: Traverse, A, B](a: T[A], b: A => ValidationNEL[Throwable, B]): ValidationNEL[Throwable, T[B]] = {
  type VNT[α] = ValidationNEL[Throwable, α]
  a.traverse[VNT, B](b)
}
PartialApply

Scalazが用意しているPartialApplyを使う手もあります。その場合は以下のようになります。

def f[T](a: List[ValidationNEL[Throwable, T]]): ValidationNEL[Throwable, List[T]] = {
  a.traverse[PartialApply1Of2[ValidationNEL, Throwable]#Apply, T](x => x)
}

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿