2012年5月24日木曜日

Scala Tips / Validation (16) - 多重度1の実装

多重度1に対応した検証とオブジェクトの生成について考えています。

case class Person(
  name: String,
  age: Int,
  address: String)

前回までに作成した部品を組合わせて作成したmakePerson関数は以下の通りです。

def makePerson(data: Map[String, String]): ValidationNEL[Throwable, Person] = {
  def value(key: String) = fetch(data, key)
  (validateName(value("name")) |@|
   validateAge(value("age")) |@|
   validateAddress(value("address")))(Person)
}

Validationでエラー情報と変換済みのデータを扱っているので、applicative演算で簡単に実装できます。

動作確認

正常データMapを用意します。

val data1 = Map("name" -> "Taro",
               "age" -> "30",
               "address" -> "Kanagawa Yokohama")

実行結果は以下の通りです。無事SuccessにくるまれたPersonオブジェクトが生成されました。

scala> makePerson(data1)
res433: scalaz.Scalaz.ValidationNEL[Throwable,Person] = Success(Person(Taro,30,Kanagawa Yokohama))
データが存在しない

年齢(age)が存在しないデータMapを用意します。

val data1nodata = Map("name" -> "Taro",
                      "address" -> "Kanagawa Yokohama")

実行結果は以下の通りです。エラー情報のExceptionを格納したFailureが生成されました。

scala> makePerson(data1nodata)
res435: scalaz.Scalaz.ValidationNEL[Throwable,Person] = Failure(NonEmptyList(java.lang.IllegalArgumentException: No value))
データエラー

データに異常があるデータMapを用意します。住所(address)が十分な長さを持っていません。

val data1bad = Map("name" -> "Taro",
                   "age" -> "30",
                   "address" -> "Yokohama")

実行結果は以下の通りです。エラー情報のExceptionを格納したFailureが生成されました。

scala> makePerson(data1bad)
res436: scalaz.Scalaz.ValidationNEL[Throwable,Person] = Failure(NonEmptyList(java.lang.IllegalArgumentException: 住所が短すぎます))
シーケンス

データがデータ列になっているデータMapを用意します。年齢(age)が3つの値のデータ列になっています。

val data1seq = Map("name" -> "Taro",
                   "age" -> "30;45;60",
                   "address" -> "Kanagawa Yokohama")

実行結果は以下の通りです。エラー情報のExceptionを格納したFailureが生成されました。

scala> makePerson(data1seq)
res437: scalaz.Scalaz.ValidationNEL[Throwable,Person] = Failure(NonEmptyList(java.lang.IllegalArgumentException: Sequence value))

ノート

Scalaプログラミングのコツというと色々あると思いますが、Monadicプログラミング向けの部品を整備しておきこれを組み合わせてアプリケーションを構築するというプログラミング戦略がかなり重要かなと思います。

今回のケースでは、ScalaモナドかつScalaz ApplicativeであるValidationを中心に、Applicative演算に適した部品を整備しました。具体的には部品を「A => ValidationNEL[Throwable, B]」の形に沿った形にしていきます。

このため、makePerson関数そのものはApplicative演算一発で実装できていますが、ここに持ってくるまでの部品整備の道筋がScalaプログラミングのコツといえるわけです。

Applicative演算とMonad演算

Applicative演算の部品として「A => ValidationNEL[Throwable, B]」の形が重要という話をしました。関数自体がこのシグネチャになっていることも重要ですし、関数の引数や返却値がこの関数になっているということ(「A => ValidationNEL[Throwable, B]」を扱う高階関数)も大事になってきます。

言うまでもありませんが、「A => ValidationNEL[Throwable, B]」はflatMapメソッドの引数そのものであり、Monad演算の軸になる関数の形です。当然ながらMonad演算の部品として非常に重要になります。

「A => ValidationNEL[Throwable, B]」はApplicative演算では、Applicative演算の文脈となるApplicative Functorの生成時に用いられます。一方、Monad演算ではApplicative演算と同様の文脈となるMonadの生成に加えて、Monad演算の軸になるflatMap関数(bind演算)そのもので用いられます。

型クラスを用いてさらに汎用化

より汎用的な表現では、MがモナドあるいはApplicative Functorである場合、「A ⇒ M[B]」の形の部品が重要になるということです。

さらに、以下のシグネチャの関数は、個別のモナドやApplicative Functorに依存しない汎用部品として機能します。

  • 「foo[M[_]: Monad, A, B](a: A): M[B]」
  • 「bar[M[_]: Applicative, A, B](a: A): M[B]」

これだけだと汎用的すぎて、実際に部品化できる処理の候補は少なそうですが(汎用部品はすでに定義されていることが多い)、以下のようにMonadやApplicativeに格納されるオブジェクトに制約を加えると、部品化の候補がぐっと広がります。

  • 「foo[M[_]: Monad, A: Monoid, B](a: A): M[B]」
  • 「foo[M[_]: Monad, A[_]: Foldable, B, C](a: A[B]): M[C]」

MonadやApplicativeに格納されるオブジェクトへの制約はMonoidなどが代表的ですが、アプリケーション固有の型クラスがあれば重要な候補となります。

正規化

Scalaプログラミングに特化しない汎用的なテクニックとしては、データの正規化があります。(正規化(normalizatin, canonicalization)というと計算機科学の方で精密な意味付けがされていると思いますが、ここではアプリケーションが用いるデータの標準化ぐらいの意味で使っています。)

データ検証の標準データをOption[Seq[String]]とすることで、部品の整備が楽になります。この効果はこれから多重度「0または1」、「1以上」、「0以上」の実装を進める中で出てくると思います。

またValidationの利用方法をValidationNEL[Throwable, T]に一本化していますが、これもデータの標準化の一種で、アプリケーションや部品の作成効率に寄与しています。

参考

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿