2012年5月31日木曜日

Scala Tips / Validation (20) - Personの実装

「多重度1」、「多重度0または1」、「多重度0以上」、「多重度1以上」の4種類の部品を作成しました。これらの部品を使って

Personオブジェクト

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

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

makePerson

前回までに作成した部品を組合わせて作成した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")) |@|
   validatePhones(value("phones")) |@|
   validateFacsimiles(value("facsimiles")))(Person)
}

「Validation (16) - 多重度1の実装」と同様に、Validationでエラー情報と変換済みのデータを扱っているので、applicative演算で簡単に実装できます。

また、validationNameやvalidatePhonesなどの関数は「A => ValidationNEL[Throwable, B]」の形になっており、この形の関数がMonadicプログラミングでは極めて重要な役割を担います。この点も、「Validation (16) - 多重度1の実装」で説明したとおりです。

もう一点重要なのが、validationNameやvalidatePhonesなどの関数が、Option[Seq[String]]を統一データ構造として採用している点です。このように統一データ構造に正規化することで、処理の共通化、簡素化を図るのが広く用いられているテクニックです。

また、細かい点ですが、汎用的なfetchメソッドを使って、makePerson関数のローカル関数valueを定義(Mapデータを束縛)して、makePerson関数内で使っています。こういうローカル関数は小回りが効いてなかなか便利です。

動作確認

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

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

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

scala> makePerson(data1n)
res81: scalaz.Scalaz.ValidationNEL[Throwable,Person] = Success(Person(Taro,30,Some(Kanagawa Yokohama),NonEmptyList(123-456-7890, 234-567-8901),List(345-678-9012, 456-789-0123)))
データが存在しない

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

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

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

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

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

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

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

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

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

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

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

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

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月30日水曜日

Scala Tips / Validation (19) - 多重度1以上の部品

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

「多重度0以上」ではListを用いましたが、「多重度1以上」ではNonEmptyListを用いるのが相違点となります。

Personオブジェクト

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

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

部品

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

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

def validateOneMore[T](f: T => Boolean, message: String, v: Option[Seq[T]]): ValidationNEL[Throwable, NonEmptyList[T]] = {
  optionSeqOneMore(v, validate(f, message, (_: T)))
}

def validatesOneMore[A, B](
  f: A => ValidationNEL[Throwable, B],
  g: Seq[B => ValidationNEL[Throwable, B]],
  v: Option[Seq[A]]
): ValidationNEL[Throwable, NonEmptyList[B]] = {
  optionSeqOneMore(v, validates(f, g, (_: A)))
}

def validatesDefOneMore[A, B](
  d: (A => ValidationNEL[Throwable, B],
      Seq[B => ValidationNEL[Throwable, B]]),
  v: Option[Seq[A]]
): ValidationNEL[Throwable, NonEmptyList[B]] = {
  validatesOneMore(d._1, d._2, v)
}

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

「多重度0」の時と同様にtraverseメソッドを用いていますが、traverseメソッドから返ってくるSeqをNonEmptyListにするための処理をmapメソッドでつないでいます。これは、Validationが成功している時のみSeqをNonEmptyListへの処理を行い、Validationが失敗している場合はFailureをそのまま使用するという処理です。

検証関数

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

def validatePhones(phone: Option[Seq[String]]): ValidationNEL[Throwable, NonEmptyList[String]] = {
  validatesDefOneMore(phoneDef, phone)
}

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

動作確認

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

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

val data1 = Map("name" -> "Taro",
               "age" -> "30",
               "address" -> "Kanagawa Yokohama")
scala> validatePhones(fetch(data1, "phones"))
res13: scalaz.Scalaz.ValidationNEL[Throwable,scalaz.NonEmptyList[String]] = Failure(NonEmptyList(java.lang.IllegalArgumentException: No value))

data1にはfacsimilesのデータがないので「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")

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

scala> validatePhones(fetch(data1n, "facsimiles"))
res74: scalaz.Scalaz.ValidationNEL[Throwable,scalaz.NonEmptyList[String]] = Success(NonEmptyList(123-456-7890, 234-567-8901))

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月29日火曜日

Object-Functional Analysis and Designふたたび

5月28日(月)にJJUG CCC 2012 Springで、『Object-Functional Analysis and Design』のセッションを行いました。予定していたセッションがキャンセルになったので、その代わりにお話させていただいた次第です。

スライド: http://www.slideshare.net/asami224/ofad

内容は基本的に3月19日(月)に要求開発アライアンスで行なった『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』の再演です。3月19日版のまとめは「Object-Functional Analysis and Designまとめのまとめ」になります。

要求開発アライアンスとJJUGではオーディエンスが違うので、事実上新規内容に近い形で見ていただけるのではないかということで、このテーマを選択しました。

前回の経験とオーディエンスの違いを勘案して再構成したのに加えて、いくつか内容の修正を行いました。ここでは、その点について記録しておきます。

再構成

要求開発アライアンスの参加者はモデリングが興味の中心と思われるので、OFADという趣旨からもモデリングの所を厚くしていましたが、今回はJavaプログラマが中心と想定されるので関数型言語のあたりを厚くしてみました。モデリングの所のスライドを減らしているのと、関数型言語のスライド数は増やしたわけではないですが、しゃべる時間を長めにしてみました。

とはいえ、JJUG CCCに参加するエンジニアはエンタープライズ系でモデリングにも興味を持っている方が多いと思われるのと、さらにこのセッションに参加される方はその傾向が大きいと思うので、モデリングに関してポイントとなるスライド(オブジェクトの世界と関数の世界, ユースケースと関数)(参考「オブジェクトの世界と関数の世界」)は残しています。さらに詳しくは「メタモデル」、「Domain-Driven Design (DDD)」あたりが面白いのですが、このあたりは省略しました。

クラウドまわりの応用でCQRSやEDAと、OFADの関係もやりたかったのですが、これはセッション時間の兼ね合いで断念しました。DCI (Data Context Interaction)はトレイトや型クラスの素材という面で面白いのですが、アーキテクチャパターンとしてはちょっと採用しづらいというのが現時点の判断なので削除しました。

新しい現実

前回: 新しい現実

今回: 新しい現実

セッションの問題設定の文脈を提示したスライドを修正しました。前回は「クラウド・プラットフォーム」、「メニーコア」、「メモリDB」でしたが、今回は「クラウド・プラットフォーム」、「メニーコア」、「DSL」にしています。

前回のスライドを作っていた時は:

  • メモリDB→I/Oボトルネックが解消→アルゴリズム勝負←メニーコア

から、関数型へのニーズが高まるというような文脈を考えていたのですが、これよりもDSLの方がはるかに影響が大きいと思うので、今回はDSLにしてみました。

関数型言語の系譜

前回: 関数型言語の系譜

今回: 関数型言語の系譜

内容に変更はないですが、図の見方についての注釈を入れました。

ボク自身は、関数型言語は大昔にLispを触って以来20年ほど空白があるので、客観的な意味での関数型言語の発展史はまったく分かりません。この図はあくまでも、Javaプログラマが2008年に関数型言語に再遭遇した時の心象風景における関数型言語の見え方です。

セッションではその旨を口頭でお話しするわけですが、スライドでの流通もあるので注釈で補足しました。

ボクと同じ世代でLispや人工知能などをかじった後、エンタープライズ系の開発を主業にされている方は、恐らくその時点での関数型言語のイメージのフィルターを通して最近の関数型言語の興隆を理解しようとすると思うのですが、モナド、型クラスという新しい言語機能が入っている現代の関数型言語は、全く別物なのでそのあたりの注意を喚起したいというのが、このスライドの趣旨です。

関数型言語の正しい発展史はボクも興味があるので、URLや書籍をお知らせ頂けると助かります。

ユースケースと関数

前回: ユースケースと関数

今回: ユースケースと関数

ユースケースと関数の関係を定義するメタモデルを前回と今回で修正しました。修正点は以下のものです。

  • OOPから関数へのリンクの元を状態遷移からサービスにした。

「EDAとオブジェクトと関数」で説明したように、EDAアーキテクチャをとりつつ状態遷移モデルは深く考えないのが、現実解ではないかというのが最近のボクの考えです。

この点を加味して、サービスから関数を直結し、状態遷移はサービスの専有下にしてみました。このアーキテクチャは、「オブジェクトと関数の連携(2)」にも沿っています。

このスライドの図を、EDAベースで実現すると「EDAとオブジェクトと関数」の最後の図になります。

CQRS/EDAとOFADを合わせてだいたいこんな感じが落としどころかなというのがボクの現時点での結論です。この図を使ってもよかったのですが、EDAの説明などが時間的に難しいので、モデリング段階の抽象的な枠組みのみにしました。

並列プログラミング

今回: 並列プログラミング

関数型言語というと並列プログラミングなので情報を追加しました。

基本的には:

  • shared mutability
  • isolated mutability
  • immutable

の三段階があって、一番下のimmutableが上策ということです。このimmutableを関数プログラミング方式で実現します。

さらにisolated mutabilityをアクター、shared mutabilityをSTMでハンドリングし、どうしてもダメな場合の最後の手段として伝統的な排他制御の技法(Javaのクラスライブラリが充実した機能セットを提供)用いるのが関数プログラミング流ということになります。

参考情報

要求開発アライアンス版に関する参考情報です。

スライド

要求開発アライアンスで使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

まとめ

セッション後のまとめは以下の記事になります。

まとめのまとめ

最終のまとめは以下の記事になります。

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

2012年5月25日金曜日

Scala Tips / Validation (17) - 多重度0または1の部品

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

今回は「多重度0または1」の部品を作ります。

Personオブジェクト

Personオブジェクトを「多重度0または1」、「多重度1以上」、「多重度0以上」を使ったものに更新します。

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

属性addressが「多重度0または1」、属性phonesが「多重度1以上」、属性facsimilesが「多重度0以上」です。

部品

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

def optionSeqZeroOne[A, B](v: Option[Seq[A]], f: A => ValidationNEL[Throwable, B]): ValidationNEL[Throwable, Option[B]] = {
  v match {
    case Some(Nil) => none.successNel
    case Some(x :: Nil) => f(x).map(_.some)
    case Some(_ :: _) => SequenceValueFailure
    case None => none.successNel
  }
}

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

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

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

ポイントになるのはoptionSeqZeroOne関数です。ロジック的には難しくありませんが、OptionとListのネストをパターンマッチングで切り分けているのがScalaらしいコーディングです。

検証関数

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

def validateName(name: Option[Seq[String]]): ValidationNEL[Throwable, String] = {
  validatesDefOne(nameDef, name)
}

def validateAge(age: Option[Seq[String]]): ValidationNEL[Throwable, Int] = {
  validatesDefOne(ageDef, age)
}

def validateAddress(address: Option[Seq[String]]): ValidationNEL[Throwable, Option[String]] = {
  validatesDefZeroOne(addressDef, address)
}

validateName関数とvalidateAge関数は「多重度1」と同じです。

validateAddress関数を「多重度0または1」向けに更新しました。先程作ったvalidatesDefZeroOne関数を利用しています。値があった場合はSuccess[Some[String]]、なかった場合はSuccess[None]を返します。

動作確認

実際に動かしてみましょう。

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

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

validateName, validateAge, validateAddress関数は以下のように正しい値を入力にして、いずれもSuccessを返しました。validateAddress関数ではSuccess[String]ではなく、Success[Some[String]]を返すのが「多重度0または1」の効果です。

scala> validateName(fetch(data1, "name"))
res439: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Taro)

scala> validateAge(fetch(data1, "age"))
res442: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(30)

scala> validateAddress(fetch(data1, "address"))
res443: scalaz.Scalaz.ValidationNEL[Throwable,Option[String]] = Success(Some(Kanagawa Yokohama))

つぎは「多重度0または1」を検証するために住所(address)のないMapです。

val data01 = Map("name" -> "Taro",
                 "age" -> "30")

validateName, validateAge, validateAddress関数は以下のように正しい値を入力にして、いずれもSuccessを返しました。validateAddress関数ではSuccess[None]を返しています。「多重度0または1」としては成功で、値は設定されていないことを通知しています。

scala> validateName(fetch(data01, "name"))
res444: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Taro)

scala> validateAge(fetch(data01, "age"))
res445: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(30)

scala> validateAddress(fetch(data01, "address"))
res446: scalaz.Scalaz.ValidationNEL[Throwable,Option[String]] = Success(None)

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

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

2012年5月23日水曜日

Scala Tips / Validation (15) - 多重度1の部品

「Validation (14) - オブジェクトの生成」でValidationを使って検証とオブジェクトの生成を行いました。

検証とオブジェクトの生成に対して検証対象に多重度を加えることにします。

多重度は「1」、「0または1」、「1以上」、「0以上」の4種類を扱うのが現実解であることを説明しました。まず、この4つの多重度の実現方法について考えていきます。

課題

以下のPersonオブジェクトを生成する、makePerson関数の実現方法について考えます。

case class Person(
  name: String,
  age: Int,
  address: String)
def makePerson(data: Map[String, String]): ValidationNEL[Throwable, Person] = {
  sys.error("これから実装")
}

以下のようなMapデータの内容を検証し、正しいデータである場合にPersonを生成します。

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

この課題ではPersonオブジェクトの3つの属性name, age, addressを多重度1として扱います。

前回までは、検証関数に必ず値が渡されてきましたが、今回はMap内に値が格納されていないケースがあります。多重度1の場合は必ず値が必要になるので、Map内に値が格納されていないケースは検証エラーとする必要があります。

前提

ここまでに作ってきた部品(本記事後ろにある「これまでの部品」参照)を使用します。この部品の上に多重度1を扱うための部品を構築していきます。

多重度のハンドリング

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

val NoValueFailure = new IllegalArgumentException("No value").failNel
val EmptyValueFailure = new IllegalArgumentException("Empty value").failNel
val SequenceValueFailure = new IllegalArgumentException("Sequence value").failNel

def optionSeqOne[A, B](v: Option[Seq[A]], f: A => ValidationNEL[Throwable, B]): ValidationNEL[Throwable, B] = {
  v match {
    case Some(Nil) => EmptyValueFailure
    case Some(x :: Nil) => f(x)
    case Some(_ :: _) => SequenceValueFailure
    case None => NoValueFailure
  }
}

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

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

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

ポイントになるのはoptionSeqOne関数で、ここで多重度1に対する検証を行っています。多重度以外の検証項目については、引数として渡されてきた関数(型はA => ValidationNEL[Throwable, B])を使用します。

optionSeqOne関数では、入力データをStringではなく、型パラメタAで受けています。こうすることで、入力データがString以外の場合も使用できるようになり、より汎用的に活用できる可能性が高まります。

データの正規化

多重度を扱うための検討事項として、検証処理が扱うデータの正規化があります。データ処理を考える場合、各データを統一的なフォーマットにしておくことで、処理を効率的に実装することができます。

4種類の多重度「1」、「0または1」、「1以上」、「0以上」をすべて表現できるフォーマットということで、今回はOption[Seq[String]]を標準フォーマットにします。入力データを一旦この形に正規化することで、後続のデータ処理がやりやすくなります。

Mapに格納されているデータを、この形で取り出す関数fetchは以下のようになります。

def fetch(data: Map[String, String], key: String): Option[Seq[String]] = {
  data.get(key).map {
    case null => Nil
    case "" => Nil
    case x => x.split(";").toList
  }
}

Mapにデータが格納されていない場合はNone、データが格納されていた場合は文字列を「;」を区切り記号にしてデータ列をSome[Seq[String]]として取り出します。

検証関数

ここまでで作ってきた部品を使って以下の多重度1用の検証関数を作成しました。

def validateName(name: Option[Seq[String]]): ValidationNEL[Throwable, String] = {
  validatesDefOne(nameDef, name)
}

def validateAge(age: Option[Seq[String]]): ValidationNEL[Throwable, Int] = {
  validatesDefOne(ageDef, age)
}

def validateAddress(address: Option[Seq[String]]): ValidationNEL[Throwable, String] = {
  validatesDefOne(addressDef, address)
}

実際に動かしてみましょう。

使用するデータを再掲します。

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

validateName, validateAge, validateAddress関数は以下のように正しい値を入力にして、Successを返しました。

scala> validateName(fetch(data1, "name"))
res423: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Taro)

scala> validateAge(fetch(data1, "age"))
res424: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(30)

scala> validateAddress(fetch(data1, "address"))
res426: scalaz.Scalaz.ValidationNEL[Throwable,String] = Success(Kanagawa Yokohama)

Mapに値が設定されていない場合は、以下のようにFailureになりました。

scala> validateName(fetch(Map.empty, "name"))
res428: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: No value))

Mapに設定されている値が異常だった場合は、以下のようにFailureになりました。

scala> validateAddress(fetch(Map("address" -> "Yokohama"), "address"))
res429: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: 住所が短すぎます))

Mapに設定されている値がデータ列だった場合は、以下のようにFailureになりました。

scala> validateName(fetch(Map("name" -> "Taro;Hanako"), "name"))
res431: scalaz.Scalaz.ValidationNEL[Throwable,String] = Failure(NonEmptyList(java.lang.IllegalArgumentException: Sequence value))

次回は、これらの部品を組み合わせてmakePerson関数を実装します。

前回までの部品

前回まで記事の中で作ってきた検証のための基本部品は以下の通りです。

def validate[T](f: T => Boolean, message: String, v: T): ValidationNEL[Throwable, T] = {
  v |> f.toValidation(new IllegalArgumentException(message))
}

def validates[A, B](
  f: A => ValidationNEL[Throwable, B],
  g: Seq[B => ValidationNEL[Throwable, B]],
  value: A
): ValidationNEL[Throwable, B] = {
  g.foldLeft(f(value)) { (a, x) =>
    a.flatMap(x)
  }
}

def validatesDef[A, B](
  d: (A => ValidationNEL[Throwable, B],
      Seq[B => ValidationNEL[Throwable, B]]),
  v: A
): ValidationNEL[Throwable, B] = {
  validates(d._1, d._2, v)
}

def intValue = (_: String).parseInt.liftFailNel

def stringValue = (_: String).success[NonEmptyList[Throwable]]

def greaterEqual[T: Order](value: T, message: String) = {
  validate(value.lte, message, (_: T))
}

def lessEqual[T: Order](value: T, message: String) = {
  validate(value.gte, message, (_: T))
}

def maxLength[T](conv: T => Int, length: Int, message: String) = {
  validate(conv(_: T) <= length, message, (_: T))
}

def maxStringLength(length: Int, message: String) = {
  maxLength((_: String).length, length, message)
}

def minLength[T](conv: T => Int, length: Int, message: String) = {
  validate(conv(_: T) >= length, message, (_: T))
}

def minStringLength(length: Int, message: String) = {
  minLength((_: String).length, length, message)
}

Personオブジェクトの検証用の個々のプロパティに対する検証の定義は以下の3つになります。

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, "住所が短すぎます")))

参考

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4