2012年4月27日金曜日

Scala Tips / Validation (10) - applicative

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

今回はapplicative演算です。

課題

以下に示す2つの関数validateNameとvalidateAgeを用いてデータの検証を行います。

  1. def validateName(a: String): ValidationNEL[Throwable, String] = {  
  2.   if (a == nullnew IllegalArgumentException("null name").failNel  
  3.   else if (a.length >= 2) a.success  
  4.   else new IllegalArgumentException("bad name: " + a).failNel  
  5. }  
  6.   
  7. def validateAge(a: Int): ValidationNEL[Throwable, Int] = {  
  8.   if (150 > a && a >= 0) a.success  
  9.   else new IllegalArgumentException("bad age: " + a).failNel  
  10. }  
validateName
正しい名前であることを検証。ここでは2文字以上の文字列を判定。
validteAge
正しい年齢であることを検証。ここでは0以上、150未満のInt値。

どちらの検証にも成功した場合はSuccess[NonEmptyList[Throwable], (String, Int)]、一つでも検証に失敗した場合はFailure[NonEmptyList[Throwable], (String, Int)]を返します。

validateNameとvalidateAgeの両方が失敗の場合には、NonEmptyListに両方のエラー情報が入ります。

flatMapやfor式を使った場合は、最初に失敗したエラー情報のみが返されますが、今回のテーマであるapplicativeの場合は、エラー情報が累積されていくのが特徴です。

(分類の基準)

Java風

Validationから直接値を取り出すことができないためキャストを使っており、その分大変見にくいコーディングになっています。その点を差し引いてもif式での判定は冗長な感じがします。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   val a = validateName(name)  
  3.   val b = validateAge(age)  
  4.   if (a.isSuccess && b.isSuccess) {  
  5.     val a1 = a.asInstanceOf[Success[NonEmptyList[Throwable], String]].a  
  6.     val b1 = b.asInstanceOf[Success[NonEmptyList[Throwable], Int]].a  
  7.     Success((a1, b1))  
  8.   } else if (a.isSuccess) {  
  9.     b.asInstanceOf[Failure[NonEmptyList[Throwable], (String, Int)]]  
  10.   } else if (b.isSuccess) {  
  11.     a.asInstanceOf[Failure[NonEmptyList[Throwable], (String, Int)]]  
  12.   } else {  
  13.     val a1 = a.asInstanceOf[Failure[NonEmptyList[Throwable], String]].e  
  14.     val b1 = b.asInstanceOf[Failure[NonEmptyList[Throwable], Int]].e  
  15.     Failure(a1 |+| b1)  
  16.   }  
  17. }  

Scala風

match式を使うとかなりすっきりしました。しかし、match式がネストするとプログラムが複雑化する感じです。この場合は判定対象が2つなのでネストはまだ浅いですが、判定対象が増えてくると大変なことになるのは明らかです。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   validateName(name) match {  
  3.     case Success(a) => validateAge(age) match {  
  4.       case Success(b) => Success((a, b))  
  5.       case Failure(e) => Failure(e)  
  6.     }  
  7.     case Failure(e1) => validateAge(age) match {  
  8.       case Success(b) => Failure(e1)  
  9.       case Failure(e2) => Failure(e1 |+| e2)  
  10.     }  
  11.   }  
  12. }  

Scala

fold, map, flatMapを使ったよい記述方法はないと思います。「Scala風」のmatch式を使った書き方になります。

Scalaz

applicative演算を使うと以下のようになります。コーディングが簡単な|@|メソッドを使っています。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   (validateName(name) |@| validateAge(age))((_, _))  
  3. }  

数学記号を使った「⊛」メソッドを使うこともできます。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   (validateName(name) ⊛ validateAge(age))((_, _))  
  3. }  

applicative演算の引数の個数が2つなので<**>メソッドを使うことができます。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   (validateName(name) <**> validateAge(age))((_, _))  
  3. }  

どの記述方法を選ぶのかは好みによりますが、いずれにしても「Java風」、「Scala風」と比べて劇的に簡潔になっています。

ノート

ScalazでApplicativeを使う場合の論点は3つあります。

  • Monad未満Functor以上でも一定の性質(Applicative演算が適用可能)を持つオブジェクトを(Applicative演算を用いて)簡潔に操作したい。
  • 複数のモナドを引数に取る演算を行いたい。
  • 複数のモナドから新しいモナドを生成する際の文脈の再構築に関して、定型処理を自動化したい。

今回のテーマであるValidationは、後ろ2つに当てはまります。

Monad未満Functor以上の性質は、Scalazの場合はZipper、ZipStreamが当てはまるようです。ただし、普通はMonadでないApplicativeはあまりないので、一般的にはモナドに対してApplicative演算を行うと考えておいてよいと思います。

「複数のモナドを引数に取る演算を行いたい」については、今回の課題では、Validationモナドの上で個々の要素に対するバリデーション結果から最終的なバリデーション結果を演算するため、Applicative演算が効果的となります。

3番目の「文脈の再構築」での「定型処理」の典型例はモノイドによる加法演算です。Validationの場合には、エラー情報をモノイドで蓄積していくようになっています。今回の例ではNonEmptyListがMonoidなので、ここに対してThrowableが蓄積されていきます。

別の書き方

Either (12) - Applicativeの記述方法」でも取り上げましたが、以下のような記述方法も可能です。これがApplicative演算の本来的な意味に忠実な記法です。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   validateName(name) <*> (validateAge(age) <*> ((x: Int) => (y: String) => (y, x)).success)  
  3. }  

また、mapメソッドを使って関数を持ち上げる処理を少し簡略化する方法もあります。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   validateName(name) <*> (validateAge(age).map(x => y => (y, x)))  
  3. }  

いずれにしても、本文で紹介した「|@|」や「<**>」の方が便利なので、普通はそちらの方を使っておくのがよいでしょう。応用によってはこちらの記法で書いた方がよいこともあるので、引き出しにストックしておきたい所です。

タプル

Applicative演算が可能な場合<|*|>メソッドで直接タプルを生成することができます。今回の課題ではこれを使用することもできます。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, (String, Int)] = {  
  2.   (validateName(name) <|*|> validateAge(age))  
  3. }  
fold

参考に「Scala」的なfoldを使ったコーディングを考えてみました。演算結果がタプルになるとfoldとの相性がわるいので、Listにしています。Listの場合、タプルより型情報の記述力が落ちるので、本文の課題より脆弱なプログラムになります。

  1. def validate(name: String, age: Int): ValidationNEL[Throwable, List[Any]] = {  
  2.   List(validateName(name), validateAge(age)).foldLeft((List[Any](), List[Throwable]())) {  
  3.     (a, x) => x match {  
  4.       case Success(s) => (a._1 :+ s, a._2)  
  5.       case Failure(e) => (a._1, a._2 ::: e.list)  
  6.     }  
  7.   } match {  
  8.     case (xs, Nil) => Success(xs)  
  9.     case (_, x :: xs) => Failure(nel(x, xs))  
  10.   }  
  11. }  

この場合は、引数の数が2つなので、かなり冗長な感じがしますが、引数が多数になる場合、引数の個数が任意個になる場合には有効なアプローチです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿