2012年5月28日月曜日

Scala Tips / Validation (18) - 多重度0以上の部品

Validation (15) - 多重度1の部品」に続き「多重度0または1」、「多重度1以上」、「多重度0以上」向けの部品を作っています。前回は「多重度0または1」の部品を作りました。

今回は「多重度0以上」の部品を作ります。順番からすると「多重度1以上」ですが、少し簡単な「多重度0以上」の方を先に取り上げます。

Personオブジェクト

Personオブジェクトの属性facsimilesが「多重度0以上」となります。

case class Person(
  name: String,
  age: Int,
  address: Option[String],
  phones: NonEmptyList[String],
  facsimiles: List[String])

部品

「多重度0以上」のハンドリングを行うための部品として以下の関数を作成しました。

def optionSeqZeroMore[A, B](v: Option[Seq[A]], f: A => ValidationNEL[Throwable, B]): ValidationNEL[Throwable, List[B]] = {
  type VNT[A] = ValidationNEL[Throwable, A]
  v match {
    case Some(Nil) => EmptyValueFailure
    case Some(x) => x.toList.traverse[VNT, B](f)
    case None => NoValueFailure
  }
}

def validateZeroMore[T](f: T => Boolean, message: String, v: Option[Seq[T]]): ValidationNEL[Throwable, List[T]] = {
  optionSeqZeroMore(v, validate(f, message, (_: T)))
}

def validatesZeroMore[A, B](
  f: A => ValidationNEL[Throwable, B],
  g: Seq[B => ValidationNEL[Throwable, B]],
  v: Option[Seq[A]]
): ValidationNEL[Throwable, List[B]] = {
  optionSeqZeroMore(v, validates(f, g, (_: A)))
}

def validatesDefZeroMore[A, B](
  d: (A => ValidationNEL[Throwable, B],
      Seq[B => ValidationNEL[Throwable, B]]),
  v: Option[Seq[A]]
): ValidationNEL[Throwable, List[B]] = {
  validatesZeroMore(d._1, d._2, v)
}

ポイントになるのはoptionSeqZeroMore関数です。

Mapにデータは存在するものの、その内容が空の場合はEmptyValueFailure、Mapにデータが存在しない場合はNoValueFailureにしています。

それ以外の場合はSeq全体を受け取り、traverseメソッドで各要素の検証を関数fで行った結果を、ValidationNEL[Throwable, List[B]]の形に変換しています。

traverseメソッドを使わない場合は、一度List[ValidationNEL[Throwable, B]]の形にした後、これをValidationNEL[Throwable, List[B]]の形に変換する必要があります。

List[ValidationNEL[Throwable, B]]からValidationNEL[Throwable, List[B]]の形への変換は、Validationを扱うプログラムでは頻出なので、一発で処理ができるtraverseメソッドは非常に重宝します。

使い方のポイントとしては、traverseメソッドは型パラメータとして[M[_], A]を取りますが、コンテナ側(M)の型パラメータ数が1つなので、引数の型をこの形に合わせる必要があります。そこで「type VNT[A] = ValidationNEL[Throwable, A]」として新しい型を定義してこれを型パラメータとして指定します。

検証関数

まず、各属性の値域の定義にphonesとfacsimilesを加えます。以前に作ったname, age, addressと合わせて以下のものになります。phonesとfacsimilesは文字数のみの簡略版ですが、実際の応用では正規表現などを使ってより精密に値域を定義することになります。

val ageDef = (
  intValue,
  List(greaterEqual(0, "年齢が0より小さいです"),
       lessEqual(150, "年齢が150より大きいです")))

val nameDef = (
  stringValue,
  List(maxStringLength(10, "名前が長すぎます"),
       minStringLength(2, "名前が短すぎます")))

val addressDef = (
  stringValue,
  List(maxStringLength(100, "住所が長すぎます"),
       minStringLength(10, "住所が短すぎます")))

val phoneDef = (
  stringValue,
  List(maxStringLength(12, "電話番号が長すぎます"),
       minStringLength(12, "電話番号が短すぎます")))

val facsimiliDef = (
  stringValue,
  List(maxStringLength(12, "FAX番号が長すぎます"),
       minStringLength(12, "FAX番号が短すぎます")))

ここまでで作ってきた部品を使って新しいPersonオブジェクト向けに検証関数を作成しました。

def validateFacsimiles(facsimili: Option[Seq[String]]): ValidationNEL[Throwable, List[String]] = {
  validatesDefZeroMore(facsimiliDef, facsimili)
}

validateFacsimiles関数を「多重度0以上」向けに新規作成しました。先程作ったvalidatesDefZeroMore関数を利用しています。値があった場合はSuccess[Some[List[String]]]、なかった場合はSuccess[None]を返します。

動作確認

新規に作ったvalidateFacsimiles関数を実際に動かしてみましょう。

まず「多重度1」と同じものを試してみます。

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

data1にはfacsimilesのデータがないので「No Value」のエラーになりました。

scala> validateFacsimiles(fetch(data1, "facsimiles"))
res68: scalaz.Scalaz.ValidationNEL[Throwable,List[String]] = Failure(NonEmptyList(java.lang.IllegalArgumentException: No value))

次は「多重度0以上」と「多重度1以上」のデータを追加したデータMapです。

val data1n = Map("name" -> "Taro",
               "age" -> "30",
               "address" -> "Kanagawa Yokohama",
               "phones" -> "123-456-7890;234-567-8901",
               "facsimiles" -> "345-678-9012;456-789-0123")

validateFacsimiles関数は以下のように正しい値を入力にしてSuccessを返しました。

scala> validateFacsimiles(fetch(data1n, "facsimiles"))
res72: scalaz.Scalaz.ValidationNEL[Throwable,List[String]] = Success(List(345-678-9012, 456-789-0123))

ノート

Validationを操作する際に、List[ValidationNEL[Throwable, T]をValidationNEL[Throwable, List[T]]に変換する処理が頻出します。

また、Validationに限らず、モナドをひっくり返す、モナドの入れ子関係を逆転させる、すなわちM[N[T]]をN[M[T]]に変換する処理はMonadicプログラミングでは頻出します。

この変換はtraverseメソッドを使って簡単に行うことができます。Validationを使う上での必須イディオムといえるでしょう。

今回の例では、optionSeqOneMoreメソッドの実装で以下のように使用しています。

  • x.toList.traverse[VNT, B](f)

fは「A => ValidationNEL[Throwable, B]」の関数です。これをList[ValidationNEL[Throwable, A]]のtraverseに適用するのでその結果「ValidationNEL[Throwable, List[B]]」が得られるわけです。

個人的なノウハウですが、traverseという名前から受けるイメージ(XMLを走査する等)と実際の動作が違っていてピンと来ませんでしたが、mapMという名前で考えるようにして、プログラミング時の混乱をなくすことができました。 

型パラメータ

traverseメソッドには型パラメータとして、VNTとBを指定していますが、その内VNTは以下のものを定義して用いています。

  • type VNT[A] = ValidationNEL[Throwable, A]

これは本文でも説明しましたが、traverseメソッドは型パラメータとして[M[_], A]を取りますが、コンテナ側(M)の型パラメータ数が1つなので、引数の型をこの形に合わせる必要があるためです。

Validationは型パラメータを2つ取るので、色々なところで型パラメータを1つの形に持ち込む必要があります。その場合は上記のVNTのような型を定義して使うとよいでしょう。(別の指定方法もありますが、これは別途説明したいと思います。)

このブログではValidationは「ValidationNEL[Throwable, A]」に統一するのがイディオムとして採用していますが、この方針に沿う場合は上記のVNTをそのまま使い回すことができます。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

0 件のコメント:

コメントを投稿