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)」あたりが面白いのですが、このあたりは省略しました。

クラウドまわりの応用でCQRSEDAと、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

2012年5月22日火曜日

Scala Tips / 多重度の表現

オブジェクトのモデリングでは属性(attribute)や関連(association)の多重度(multiplicity)が重要なモデル要素になっています。

今回は、Scalaで多重度を表現する方法について考えます。

多重度

代表的な多重度は以下の4つです。表ではXMLのDTDによる指定方法を併記しました。UMLではもっと細かい条件も指定できますが、実用的にはこの4つで考えていくとよいでしょう。

多重度UMLDTD
11表記なし
0または10,1?
1以上1..N+
0以上0..N*

Scalaの表現

多重度をScalaで表現する代表的な方法を以下の表に示します。

多重度ScalaScalaz
1--
0または1OptionOption
1以上ListNonEmptyList
0以上ListList

多重度が1の場合は、オブジェクトをそのまま参照すればOKです。

多重度が0または1の場合、Javaだとnull値か否かで判定するのが普通でしたが、ScalaではOptionを使います。nullは例外を除いて使わないのが基本方針です。多重度0または1の表現にnull値を利用すると、変数型やメソッド引数の型、メソッド返却値の型で多重度1の場合と多重度0または1の場合の見分けがつかず、バグを温床になっていました。多重度0または1をOptionで表現することで、この問題を回避することができます。

多重度1以上の場合、ScalaではListなどのコレクションクラスを用いて表現します。表では代表してListを記述していますが、用途によって他にも選択肢があります。Listなどのコレクションクラスは多重度0の場合も使えるので、静的型付けで多重度0を排除することはできません。制約などを用いて別枠で処理を加える必要があります。

多重度1以上の場合、Scalazを使っている場合には、空でないことが保証されたリストであるNonEmptyListを使うことができます。NonEmptyListを使うことで、多重度0のケースを静的型付けで弾くことができるのは大きなメリットです。

多重度0以上の場合、Scala、ScalazともListなどのコレクションクラスを用います。ここでも代表してListを記述していますが、用途によって他にも選択肢があります。

実例

Validation (14) - オブジェクトの生成」で出てきたPersonクラスを、多重度を使ってより精密な指定をしてみました。

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

NonEmptyListは、Listと比べると使いにくいので無理をせず多重度1以上の場合も普通にListを使うという選択もあります。

コンテナの選択

多重度が「1以上」または「0以上」の場合、オブジェクトの集りを格納するためのコンテナとしてコレクションクラスを使用しましす。Scalaの代表的なコレクションクラスはListで、多くの場合はListを用いるのがよいのですが、必要に応じて選択していく必要があります。

各、コンテナの特徴は、別の機会に取り上げる予定ですが、ここでも簡単に説明しておきます。Scalaのコンテナは可変版(mutable)と不変版(immutable)がありますが、普通のScalaプログラミングをしていくとほぼ不変版しか使わないでよいので、以下では不変版のみを対象にします。

Seq
Seq
シーケンス(デフォルトはList)
List
リスト処理向けのシーケンス
Vector
ランダムアクセス向けのシーケンス
Stream
遅延評価List

結論から言うと、一般的にはListを選択しておくのが無難です。

Seq

Seqはシーケンスを記述するコンテナです。List, Vector, Streamの親トレイトとなっています。

Seqを具象オブジェクトとして使うと、実装はListが使用されます。このため、型としてListよりもSeqが望ましい場合にSeqをコンテナとして使うことになります。

List

Listは以下の特徴を持っています。

  • LinearSeq
  • 永続データ構造として使いやすい
  • ケースクラス「::」によるパターンマッチング
  • 「::」メソッド、「:::」メソッド

「::」メソッドと「:::」メソッドはそれぞれSeqの「+:」メソッドと「++」メソッドと同じなので、特にアドバンテージというわけではありません。Lispのリスト処理に似たような記述ができるので、見栄えがよくなるという効用が期待できます。

Listは再帰的かつ永続データ構造のコンテナなので、関数型で多用する再帰的なアルゴリズムとの相性がよいのが美点です。また、ケースクラス「::」によるパターンマッチングが必要かどうかもListを選ぶ際のポイントになります。

Scalaプログラミングでは、「迷ったらList」と考えておくとよいでしょう。

Vector

Vectorは以下の特徴を持っています。

  • IndexedSeq

IndexedSeqは、インデックスによるランダムアクセス向きのSeqであることを示しています。また、データを配列で持つので大規模データを効率よく格納、アクセスすることが期待できます。

まとめると以下のユースケースに適したコンテナです。

  • 永続データ構造的なアルゴリズムを使わない
  • 大規模データ
  • ランダムアクセスを行う
Stream

Streamは遅延評価のListで以下の特徴を持っています。

  • LinearSeq
  • 遅延評価
  • オブジェクト「#::」によるパターンマッチング
  • 「#::」メソッド、「#:::」メソッド

Streamは遅延評価なので用途が限定されます。

Set

Setは集合、つまりデータの重複がないオブジェクトの集りを格納するコンテナです。

Set
集合(デフォルトはHashSet)
HashSet
実装にハッシュを用いた集合(順序が保証されない)
ListSet
実装にListを用いた集合(順序が格納逆順)
SortedSet
順序が整列順になる集合(デフォルトはTreeSet)
TreeSet
実装に木構造を用いた集合(順序が整列順)

Setは、個々のコンテナの特徴がはっきりしているので、用途に応じて選択していけばよいでしょう。

実例2

色々なコンテナを説明してきました。例えば、Setを用いてPersonを以下のように表現することもできます。

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

この場合には、phones(電話番号)とfacsimile(FAX番号)はそれぞれ重複した値を持たないことを静的型付けで保証しています。

2012年5月21日月曜日

MindmapModeling「燃えるスカイツリー争奪戦」

5月19日(土)に横浜モデリング勉強会(facebook group)を行いました。また、会場には(株)アットウェア様の会議室をお借りしました。参加された皆さん、アットウェア様、どうもありがとうございました。

この勉強会で、浅海が作成したモデルを紹介します。モデルはMindmapModelingの手法で作成しました。(勉強会で使用したチュートリアル)

前々回、前回と(1)MindmapModelingでドメイン・モデルの作成、(2)SimpleModelerでモデル駆動開発を試してみましたが、2コマ目のモデル駆動開発のための回が少し間延びするかな、ということで、今回から以下のようにすることにしました。

  • 記事の内容を元に、ドメイン・モデルを作成し、この上にサービス・モデルを作成。
    • ドメイン・モデルとサービス・モデルは、DSL駆動開発での開発を念頭においたものにする。
  • 時間内に、(1)ドメイン・モデル、サービス・モデルの作成、(2)SimpleModelerで参考実装の生成、(3)サービスの開発をできるところまで行う。
  • 勉強会終了後、宿題としてサービスの開発などを行った場合は、次回の勉強会の最初にレビュー会を行いフォローする。

勉強会の時間内ではモデリング+αがいいところなので、DSL駆動開発を前提にしたモデリングをすることと、モデルからどのような参考実装が生成されるかの確認をするところまでが想定した作業になります。

ドメイン・モデルを作成するためには、サービス・モデルの文脈が確定している必要があるので、ドメイン・モデルとサービス・モデルは相互に協調してスパイラル的に構築されていくことになります。

テーマ

モデリングの対象は、日経ビジネス誌の記事「燃えるスカイツリー争奪戦」です。

記事の方は4つぐらいの小さな記事の集りで、モデリングの素材としては今ひとつでした。面白そうなモデルをつくりにくいので、もう一つ別の記事も用意しましたが、こちらもモデリングの対象としては今ひとつのようでした。

そこでボクは、スカイツリーの記事から人材仲介会社のビジネスをテーマにすることにしました。

用語の収集

ドメイン・モデルの土台となる用語を収集してラフに分類します。今回は記事の内容がモデリング対象としては浅いので、用語の収集だけだとモデルとしてはしごくあっさりしたものになります。


サービスを念頭においた作り込み

システムづくりのために、モデルの内容を追加していきます。


物語

大事なのが、物語作り。

システムをビジネスと連動させるためには、ビジネス上の「物語」の中でシステムがどのような局面(文脈)でどのような役割を果たして行くのか、という点が明確になっている必要があります。

このための鍵となるモデル要素が「物語」です。「物語」を使って、出来事→(登場人物, 道具)を束ね、ビジネスの進行に伴う登場人物や道具の状態遷移を確定します。


今回の作業はここまでで、DSLを使ったコード生成まではできませんでした。次回は時間配分を調整して、コード生成まで行くようにしたいと思います。

ビジネス・モデリング

Scalaのような「DSL指向言語」の興隆で、システム開発もさらにプログラミング主導の方に軸足が移っていきそうです。

その流れの中で、なぜシステム開発でのモデリングを重視しているのかというと、ビジネスとシステムをきちんと連携させ、意味のあるシステム開発を進めていくには、ビジネス・モデルからシステム・モデルへのモデル上の追跡性が重要となると考えているからです。

ビジネス上の要件が、システムのどのモデルのどの仕様に影響を与えているのかをきちんと管理できていないと、持続可能性のあるシステム開発を行うことができません。少人数のビジネスでビジネスオーナーとプログラマが一体化しているケースではプログラム主導でもいいのですが、ステークホルダーが一定数を超えてくると、共通のビジョンを共有するためのモデルづくりが必要になってきます。

そのためのモデリングの手法として、SimpleModelingやMindmapModelingを整備しているわけです。

このあたりのモデリングの位置付けに質問があったので、勉強会では板書を使って説明を行いました。



OO技術によるビジネス・モデリング手法と、このビジネス・モデルとOOADへの連携の話題は2000年代前半には盛んでしたが、IT技術のホットスポットがWeb技術の加速度的な進展に移ってしまった2000年代中盤以降はロストテクノロジー化してしまっているように思います。

また、この影響もあってビジネス・モデリングからOOADへの具体的なモデルの連携方法は大枠の方向性は定まっているものの、具体的に教育可能なレベル(名人芸に依存しない)、DSLで扱うレベル(可能な範囲で自動生成)の詳細化、具体化という点では未整備のままになっている、というのがボクの認識で、このあたりのミッシングリンクを埋めるのがSimpleModeling、MindmapModeling、SimpleModelerの目標になっています。

Web技術の方は、クラウドの方に大きくシフトして、依然として加速度的な進化は続いていますが、Web/クラウド技術の優位性について一定の評価が確定したのではないかと思います。そういう意味で、そろそろ企業システムも情報系あたりからWeb/クラウド上に載せ替える時期に入って来ることになりそうです。

そのような流れの中でロストテクノロジ化してしまったOOAD( +ビジネス・モデリング)が再度必要になってくるのではないかと期待しています。もちろん、新しいOOADは新しい革袋の中に入って新しい枠組みの方法論として登場してくるはずです。それがどのようなものになるのか分かりませんが、本ブログもDSLやOFADについて具体的に考えていくことで、新しい枠組みに迫っていければと思っています。

2012年5月18日金曜日

Scala Tips / Validation (14) - オブジェクトの生成

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

今回はここまで調べてきた以下の技法を使って、Validationの代表的なユースケースであるオブジェクトの生成について考えます。

課題

文字列のデータから以下のPersonオブジェクトを生成します。

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

name, age, addressの各属性は、以下の関数を使って検証します。

  • validateName
  • validateAge
  • validateAddress

この検証がすべて成功した場合のみPersonオブジェクトを生成します。

Personオブジェクトを生成する関数は以下のシグネチャになります。結果はValidationNEL[Throwable, Person]に格納します。

  • f(name: String, age: String, address: String): ValidationNEL[Throwable, Person]

準備

前回(「Validation (13) - toValidate」)に用意した部品です。

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 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))
}

前回に使用した定義です。

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

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

新しい部品と定義

今回は以下の部品を追加します。

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

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)
}

新しく名前と住所の検証を行う関数を追加しました。ここでは、文字列の長さの検証を行っていますが、実務で使うような場合は文字列のパターンや使って良い文字、NGワードの判定などを行うことになるでしょう。

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

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

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

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

Personオブジェクトの生成

Validationを使う場合の定番のイディオムがApplicative演算(「Validation (10) - applicative」)です。以下のようにApplicativeを使うと一発で処理を記述することができます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))(Person)
}

ノート

上の処理と同じことを普通に書いていくと以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  validateName(name) match {
    case Success(na) => validateAge(age) match {
      case Success(ag) => validateAddress(address) match {
        case Success(ad) => Person(na, ag, ad).success
        case Failure(ead) => ead.fail[Person]
      }
      case Failure(eag) => validateAddress(address) match {
        case Success(ad) => eag.fail
        case Failure(ead) => (eag |+| ead).fail
      }
    }
    case Failure(ena) => validateAge(age) match {
      case Success(ag) => validateAddress(address) match {
        case Success(ad) => ena.fail
        case Failure(ead) => (ena |+| ead).fail
      }
      case Failure(eag) => validateAddress(address) match {
        case Success(ad) => (ena |+| eag).fail
        case Failure(ead) => (ena |+| eag |+| ead).fail
      }
    }
  }
}

パラメタが3つなので、2の3乗で8パターンの判定が必要になります。これでもかなり煩雑ですし、単純な処理ではあるものの手作業でコーディングしていくとエラーが混入してしまいそうです。また、パラメタが増えてくると判定パターンが増えてきて破綻してしまうのは明らかです。

こういった共通処理を一つにまとめているのがApplicative(+Monoid)というわけです。関数型プログラミングの威力は、手続き型やオブジェクト指向では共通化、部品化できなかったこういった定型的なアルゴリズムを部品化して再利用できることにあります。そのための道具として圏論(Applicative)や群論(Monoid)といった数学理論が利用されているわけですね。

for式

Applicativeのある所にfor式あり。ということでfor式を使って以下のように書くこともできます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  for {
    a <- validateName(name)
    b <- validateAge(age)
    c <- validateAddress(address)
  } yield Person(a, b, c)
}

for式はScalaz ApplicativeではなくScalaモナドをターゲーットにしていますが、Scalaz ApplicativeのオブジェクトはおおむねScalaモナドにもなっているので、一般的にはApplicativeでやっている処理をfor式でも記述することができると考えてもよいでしょう。(「Validation (11) - モナド」で説明したとおり、ValidationはScalaz Monadではないものの、Scalaz ApplicativeかつScalaモナドという、やや特殊な性質のオブジェクトになっています。)

Promiseを使った並列計算などのようにApplicativeとfor式で動きが違うケースもあるので、必ずしも相互変換可能というわけではないので、注意は必要です。

こうやってみていくとfor式はかなり強力ですね。Monadicプログラミング的にも迷ったらfor式という方針で臨むのがよいでしょう。

クロージャ

本文の例は、Scalaのクロージャの記述方式を最大限に利用して、最大限の省略を行っていますが、慣れていない人にはコードゴルフ的なコードともいえます。

省略を行わず丁寧に書くと以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))((nm, ag, ad) => Person(nm, ag, ad))
 }

少しだけ省略して以下のように書くこともできます。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address))(Person(_, _, _))
}

このあたりはコードをたくさん書いて慣れていくしかありません。

タプル

Applicative演算や関数呼び出しを汎用的な枠組みで扱いたい場合に、タプルを使うケースが多々あります。

ついでなので、今回のケースであえてタプルを介在させてみましょう。

Validation (10) - applicative」で説明したように演算子「|@|」で生成されるApplicativeBuilderのtupledメソッドで、Applicative演算の結果を格納したタプルを生成することができます。 タプルは、ValidationNEL[Throwable, Tuple3[String, Int, String]]という形でValidationに格納されることになります。このValidationモナドの文脈上で、mapメソッドを用いてタプルからPersonオブジェクトの生成を行えばよいわけです。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) |@| validateAge(age) |@| validateAddress(address)).tupled.map(Person.tupled)
}

Applicative演算でタプルを生成する場合には、演算子「<|**|>」を用いる方法もあります。この場合には以下のようになります。

def f(name: String, age: String, address: String): ValidationNEL[Throwable, Person] = {
  (validateName(name) <|**|> (validateAge(age), validateAddress(address))).map(Person.tupled)
}

なお、tupledメソッドは関数オブジェクトの提供するメソッドで、コンストラクタには使用することができません。今回tupledメソッドを使用できるのは、newによるオブジェクトの生成ではなくて、case classだからです。case classの場合は、内部的にファクトリオブジェクトのメソッドでオブジェクトの生成を行うことになるので、tupledメソッドを使うことができるわけです。

参考

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月17日木曜日

Scala Tips / Validation (13) - toValidatation

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

前回はparseIntメソッドなどを用いて、文字列から数値への変換を行いながらフォーマットエラーをValidationとして取り出す方法について説明しました。

次は、アプルケーションの定めた値域に対するチェックを追加する処理を考えてみます。

ScalazではFunction1[T, Boolean]にtoValidationメソッドが追加されているので、その使い心地のチェックも兼ねています。

課題

年齢の入力チェックの関数です。文字列を入力にし、0以上または150以下のInt値に合致する場合はValidation[NonEmptyList[Throwable], Int]のSuccess、それ以外の場合はエラー内容に対応する文字列をIllegalArgumentExceptionに設定したFailureを返します。

if式

普通にif式で書くと以下のようになります。これでも十分ですが、「new IllegalArgumentException」や「failNel」の文言を各判定ごとに書くのが煩雑です。また、ちょっとプログラムっぽい感じなので、できればDSL的に宣言っぽくしたいところです。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap { a =>
    if (a < 0) new IllegalArgumentException("年齢が0より小さいです").failNel
    else if (a > 150) new IllegalArgumentException("年齢が150より大きいです").failNel
    else a.successNel
  }
}

validation関数

共通処理をvalidation関数として部品化します。ポイントになるのがFunction1[T, Boolean]のtoValidationメソッドです。関数の評価結果がtrueの場合、関数の引数をValidation[NonEmptyList[Throwable], Int]のSuccessに、エラーの場合は、Failureにエラー情報を設定したものを返す関数を返します。

今回のケースではエラー情報をIllegalArgumentExceptionに設定します。

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

validation関数を使ったvalidateAge関数は以下のようになります。ValidationはモナドなのでflatMapメソッドを使って連結することができます。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap {
    x => validate((_: Int) >= 0, "年齢が0より小さいです", x)
  } flatMap {
    x => validate((_: Int) <= 150, "年齢が150より大きいです", x)
  }
}

さらに部分適用を使って、validation関数を関数オブジェクト化することで、以下のようにより簡潔に記述できます。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  age.parseInt.liftFailNel.flatMap {
    validate((_: Int) >= 0, "年齢が0より小さいです", _)
  } flatMap {
    validate((_: Int) <= 150, "年齢が150より大きいです", _)
  }
}

for式

Validationはモナドなのでfor式を使って連結することができます。for式を使った場合、余分の変数名を考えないといけないのがちょっと不満ですが、シンプルに書けるのは確かです。

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  for {
    a <- age.parseInt.liftFailNel
    b <- validate((_: Int) >= 0, "年齢が0より小さいです", a)
    c <- validate((_: Int) <= 150, "年齢が150より大きいです", b)
  } yield c
}

DSL

DSLっぽく宣言的に書くための部品を用意します。

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 validateAge(age: String): ValidationNEL[Throwable, Int] = {
  validates(
    (_: String).parseInt.liftFailNel,
    List(validate((_: Int) >= 0, "年齢が0より小さいです", _),
         validate((_: Int) <= 150, "年齢が150より大きいです",_)),
    age)
}

さらにDSL

さらにDSLを目指して以下の部品を用意します。こういった文法糖衣的なユーティリティの関数を用意するのはよく使うテクニックです。

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

def greaterEqual(value: Int, message: String) = {
  validate((_: Int) >= value, message, (_: Int))
}

def lessEqual(value: Int, message: String) = {
  validate((_: Int) <= value, message, (_: Int))
}

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)
}

この部品を使うと以下のように書くことができます。

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

def validateAge(age: String): ValidationNEL[Throwable, Int] = {
  validatesDef(ageDef, age)
}
Order

大小比較のgreaterEqual関数、lessEqual関数は、ScalazのOrderを使ってInt型に依存しない汎用的なものにすることができます。

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))
}

ノート

Function1[T, Boolean]のtoValidationメソッドはValidationではなくValicationNELを生成することもあり便利かもしれないと思って考えてみましたが、ValidationNEL[Throwable, T]を標準形とするなら、IllegalArgumentExceptionなどのExceptionを生成する処理が必要となるため結局専用の関数を用意することになり(validation関数)それほど使いどころはなさそうです。

逆にValidationNEL[String, T]でよいケースでは、なかなか便利に使えそうな感触を持ちました。

ValidationNEL[String, T]で処理を進めておいて、最後にValidationNEL[Throwable, T]にするという形にすれば、toValidationメソッドを活かして書くことができるかもしれません。

DSL

toValidation関数を使うだけだと、あまり面白くないので、値域の設定をよりDSLっぽくできるようにしてみました。こういったDSLを作る場合には、関数オブジェクトや部分適用といった関数型ならではの言語機能がとても便利です。こういった細かいところの使い勝手も「DSL指向言語」としては大事ですね。

追記 (2012-06-21)

タイトルを「Scala Tips / Validation (13) - toValidate」から「Scala Tips / Validation (13) - toValidation」に変更しました。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月16日水曜日

Scala Tips / Validation (12) - parse

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

Validationは、フォームやREST通信などでのパラメタチェックが典型的な利用例です。

その中でも、文字列を数値に変換する処理でのエラー検出は頻出処理です。Scalazでは、StringでparseIntメソッドなど以下のエラー検出用メソッドを提供しています。

  • parseBoolean
  • parseByte
  • parseShort
  • parseInt
  • parseLong
  • parseFloat
  • parseDouble

これらのメソッドの使い方についてみていきます。

parseInt

Int値を文字列から整数値に変換する処理はtoIntメソッドで行うことができますが、文字列のフォーマットがおかしかった場合、toIntメソッドはNumberFormatExceptionをスローします。NumberFormatExceptionをtry/catchで受けてエラー処理を行うのはかなり煩雑なコーディングになるのと、Monadicに処理できないのでできれば避けたいところです。Validationはこのような場合に便利なモナドです。

以下のようにtoIntメソッドを使うところで、parseIntメソッドを使います。そうすることでValidation[NumberFormatException, Int]が返ってきます。

def f(a: String): Validation[Throwable, Int] = {
  a.parseInt
}

フォーマットが正しかった場合は整数値、フォーマットエラーの場合にはデフォルト値として−1を返したい場合には以下のようにします。(Validation (1) - 値の取り出し)Validationにすることで、色々な技が使えるようになります。

def f(a: String): Int = {
  a.parseInt | -1
}

この他に、parseByteやparseDoubleなども同様の処理を行うことができます。

NonEmptyList

Validation (3) - NonEmptyList」で説明したようにエラー情報側にNonEmptyListを使うのがイディオムです。その場合は以下のようにliftFailNelメソッドを併用します。

def f(a: String): ValidationNEL[Throwable, Int] = {
  a.parseInt.liftFailNel
}
Applicative

エラー情報側にNonEmptyListを使うことによって、「Validation (10) - applicative」で説明したApplicativeを使うことができます。 

def f(a: String, b: String, c: String): ValidationNEL[Throwable, (Int, Float, Short)] = {
  (a.parseInt.liftFailNel |@| b.parseFloat.liftFailNel |@| c.parseShort.liftFailNel) tupled
}

正しい値の場合には以下のように動作します。

scala> f("10", "10.5", "100")
res60: scalaz.Scalaz.ValidationNEL[Throwable,(Int, Float, Short)] = Success((10,10.5,100))

フォーマットや値域に異常がある場合は以下のように動作します。

scala> f("10.5", "a", "100000")
res61: scalaz.Scalaz.ValidationNEL[Throwable,(Int, Float, Short)] = Failure(NonEmptyList(java.lang.NumberFormatException: For input string: "10.5", java.lang.NumberFormatException: For input string: "a", java.lang.NumberFormatException: Value out of range. Value:"100000" Radix:10))

NonEmptyListを使わない場合(正確にはエラー情報がMonoidでない場合)にはApplicativeで使用することができません。以下の関数はコンパイル・エラーとなります。

def f(a: String, b: String, c: String): Validation[Throwable, (Int, Float, Short)] = {
  (a.parseInt |@| b.parseFloat |@| c.parseShort) tupled
}

エラー情報にNonEmptyListを使わないと色々と弊害がでてくるので、Validationを使う場合にはNonEmptyListとセットにし、ValidationNELという型を使うのがイディオムとなっています。

同様に、エラー情報の中身にはExceptionではなくThrowableを使う方が便利です。この辺の事情は「Validation (6) - Exception」で説明しました。

ノート

parseIntメソッドを使わず自分でNumberFormatExceptionをハンドリングしてparseIntメソッド相当の処理を行う場合は以下のようなプログラムになります。scala.util.control.ExceptionのallCatchメソッドを使うのが簡便でよいでしょう。allCatchメソッドで返ってくるCatchオブジェクトのeitherメソッドを使ってEitherに変換し、さらにvalidationメソッドでValidationに変換します。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  validation(allCatch.either(a.toInt)).liftFailNel
}

allCatchメソッドでは、NumberFormatException以外のExceptionもキャッチしてしまうので、NumberFormatException限定にしたい場合にはcatchingメソッドを使います。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  val b = catching(classOf[NumberFormatException]).either(a.toInt)
  validation(b).liftFailNel
}

個人的には、このようなケースで個々のExceptionを頑張って拾ってもあまり益がないように思われるので(最後はエラー表示してタスクを終了するという処理になることが多いので)、おおむねallCatchを使います。

書き方

「allCatchメソッドで返ってくるCatchオブジェクトのeitherメソッドを使ってEitherに変換し、さらにvalidationメソッドでValidationに変換します」という処理を:

  • validation(allCatch.either(a.toInt)).liftFailNel

のように一行にまとめると処理が分かりづらくなります。このように関数型プログラミングでは、一行が長くなりがちなので実際のプログラミング時には工夫が必要になってきます。

一番安全なのは以下のように中間の値をval変数に入れていく方式です。美しくありませんが効果抜群で、デバッグもやりやすくなります。ボクもよく使っていて、中間のval変数名を考えるのが煩雑なので、a, b, cと連番で使うようにしています。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  val b = allCatch.either(a.toInt)
  validation(b).liftFailNel
}

一行にまとめた時に分かりづらくなる理由の一つに、処理の流れが右、左、右というように左右に移動しながら処理が進むということがあります。この問題に対する解として有力なのが、演算子「|>」です。「|>」を用いることで、処理が左から右へ一貫した方向にパイプライン上を流れるように進むように記述できます。この方式をとると一行が長くなっても苦労せずに処理を追うことができます。

def f(a: String): ValidationNEL[Throwable, Int] = {
  import scala.util.control.Exception._
  allCatch.either(a.toInt) |> validation liftFailNel
}

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月15日火曜日

Scalazの型クラス

Validationの定義を調べるついでにScalazの型クラスの継承関係などを調べたのでまとめておきます。

Monad

Monad関係の型クラスの継承関係は以下のようになります。


整理すると、Monad、Applicative Functor、Pointed Functor、Functorはそれぞれ以下の性質を持っていることになります。







FunctorPureApplyBind
Functor---
Pointed Functor--
Applicative Functor-
Monad

Functor&Pureは型クラスPointedとして定義されています。Pointed Functorと呼ぶこともあるようです。

Applicative Functorの型クラスApplicativeはFunctor&Pure&Applyとなります。Applicative Functorのapply演算は型クラスApplyが提供します。

Monadの型クラスMonadはFunctor&Pure&Apply&Bindとなります。Monadのunit演算、bind演算はそれぞれ型クラスPureと型クラスBindが提供します。Monadのjoin演算は型クラスとしては定義されていませんが、型クラスBindのbind演算から逆算する関数が定義されています。

機能順で示すと:

  • Monad > Applicative Functor > Pointed Functor > Functor

ということになります。型クラスPointed Functorは定義はありますが、実際に直接使用している所はほとんどないので実際はあまり意識することはありません。

代数的構造

代数的構造に関する型クラスの継承関係は以下のようになります。

型クラスSemigroupは半群(semigroup)、型クラスZeroは単位元(identity element)を記述する型クラスです。

型クラスMonoidは、型クラスSemigroupと型クラスZeroを継承した型クラスで、モノイド(monoid)を記述する型クラスです。

本来であれば、逆元(inverse element)群(group)があってもよさそうですが、これらは定義されていないようです。もちろん、環(ring)体(field)の定義もありません。

今の所、プログラミング言語の基本機能として有効な代数的構造はモノイド(半群、単位元)までということだと思います。

Scalazを使っていると、モノイドという数学(代数的構造)上の抽象概念が、プログラミングにおいて非常に便利なことを実感します。具体的には、モノイドは畳込み系(fold系)の演算で頻出します。

関数型プログラミングの「コメ」

モナドは、Scala本体でもflatMapメソッドのハードコーディングという荒業で実現していますが、Applicative Functorやモノイドについては今の所Scalazを使わないと利用することができません。また、モナドやファンクタも型クラスMonadや型クラスFunctorを使うことで、静的型付けの枠組みを活かしながら安全で発展的な運用を行うことができます。

Functor, Applicative Functor, Monad, Monoidが関数型プログラミングの「コメ」とするなら、現時点のScalaプログラミングではScalazを使うのが唯一の解ということになります。

並列プログラミング

代数的構造は、普通の関数型プログラミングでも有効な概念ですが、並列プログラミングではさらにその重要性が増すと考えています。ボクがScalazを使って代数的構造ベースのプログラミングの経験値を上げておきたいと考えているのも並列プログラミングが大きな動機となっています。

代数的構造に登場する結合律(associative law)、可換律(commutative law)、分配律(distributive law)は専門用語としてみると難しく感じますが、小学生の算数にも出てくる概念で、概念そのものは誰でも知っていることです。

ただし、これらの概念を任意のデータ構造とそれに対する演算に適用しようとすると、とたんに難しくなります。少なくても今までは普通のオブジェクト指向言語では、スコープ外の概念でした。

これが、HaskellやScala+Scalazといった現代風の関数型プログラミングで、始めて普通のプログラマの道具として使用できるようになったと認識しています。

この結合律、可換律、分配律が並列プログラミングでは非常に重要な概念になってくると推測しています。結合律、可換律、分配律を満たすオブジェクト群に対する演算は、処理の実行順番に対する制約が低くなるので、処理の最適化を自動化できる可能性が高まります。

メニーコア時代に入ると、一般のアプリケーションもネイティブな並列アプリケーションになってくると予測されるので、そうなれば一般のアプリケーション・プログラマにとっても基本的な素養となるということです。

結合律

結合律は現段階でもモノイドを用いて利用可能です。

たとえば、fold系の畳込みの対象がモノイドである場合には、畳込みの演算の評価順番が左からや右からといった制約がなくなるので、並列実行して処理の終わったところから畳み込んでいくといった最適化が可能になります。

具体的には「A + B + C」を「(A + B) + C」と計算しても、「A + (B + C)」と計算してもよいということですね。

分散カウンタ的なアルゴリズムをより汎用的な形で実現することにも使えそうです。分散カウンタの場合は、整数値の加算がモノイドであることを利用しているわけですが、これを任意のデータ構造に応用できるようになります。

型クラスMonoidを用いることで、こういったメカニズムを静的型付けで利用できるようになります。

可換律

結合律は型クラスMonoid(Semigroup)の導入ですでに実用化されていますが、次の課題は可換律です。

可換律を満たした演算は、結合律よりもさらに大きな最適化が可能になります。

前述のfold系の畳込みの例で言うと、畳込み対象が可換モノイドの場合は、畳込みの演算の評価順番だけでなく、演算の実行順番も変えることができます。

具体的には「A + B + C」を「(A + B) + C」と計算しても、「(A + C) + B」と計算してもよいということですね。並列処理では、どの処理がどの順番で完了するか事前に決定できないので、式の形を変形して実行順番を変えてよいというのは、非常に大きなアドバンテージになります。

前述の分散カウンタの例でも、整数の加算は可換モノイドでもあるので、この性質も利用可能です。任意のデータ構造に適用する場合も、モノイドより可換モノイドの方がより最適化された処理を実行することが可能になります。

現時点でのScalazでは定義されていませんが、いずれCommutativeSemigroupやCommutativeMonoidといった型クラスが登場して、これらの型クラスを使用した並列処理の関数などが登場するようになるのではないかと思います。Scalazでは、parMapメソッドやparBindメソッドでPromiseモナドを使った並列処理のファンクタ演算やモナド演算が使えますが、ここにCommutativeMonoidを対象としたparFoldReduceメソッドが提供されるというようなイメージです。

分配律

分配律は、二項演算が2つ登場してきて、難易度がぐっと上がってきます。代数的構造では環や体で分配律を扱いますが、プログラミングへの応用は当分先の話になりそうです。

Comonad

Monadの逆の動きをする型クラスComonad関連の型クラスです。

中心となるのが型クラスCojoinです。Scalazの型クラスには直接現れてきませんが、bind演算はmap演算+join演算です。このjoin演算がモナド演算の核となります。join演算は、入れ子になったモナドを一つにまとめる操作を行いますが、この操作時にそれぞれのモナド特有の結合処理を行うのが、モナド演算のミソとなっています。型クラスCojoinはこのjoin演算の逆演算をする型クラスです。つまり、単層のモナドから、モナドの入れ子を再構築します。

型クラスComonadは、このCojoinを利用して単層のモナドからモナドの入れ子を再構築します。

同様に型クラスCopureは型クラスPureの逆演算をする型クラスです。

Comonadは理屈は朧気(おぼろげ)ながらわかるのですが、実際のプログラミングでどのようなユースケースがあるのかは今の所判然としません。

ここでは存在を記録しておくにとどめておきます。

Category

ScalaやScalazのモナドは圏論の理論をプログラミングに応用した物です。その大元となる圏論の圏も型クラスCategoryとして型クラス化されています。

圏はオブジェクトと射(arrow、morphism)から構成されますが、この射を記述する型クラスがArrowです。

Category関連の型クラス群は、型クラスArrowの型クラスインスタンスが分からないと、プログラミングとの接点が見えてこないので、図に入れています。

  • Function1Arrow
  • PartialFunctionArrow
  • KleisliArrow
  • CokleisliArrow

現時点でのユースケースは、これらの型クラスArrowの型クラスインスタンスを用いて引数1の関数や、モナドを合成することに使用されます。

引数1の関数の合成は、Scala本体でもcomposeメソッドやandThenメソッドが用意されているので、それほどニーズがあるわけではありませんが、「関数型とデータフロー(2)」で取り上げた複雑な構成のパイプラインを構築するときには有効です。

モナドの結合はflatMapメソッドなどを使えばよいのですが、モナドを合成してより大きなモナドを作るということになるとKleisli圏が登場してきます。Kleisli圏ではモナドは射として扱われるので、射の合成としてモナドを合成することができます。

kleisli圏でのモナドの合成は並列処理を行うPromiseモナドで有力な利用方法です。flatMapなどによるモナドの結合ではflatMapのポイントで並列処理がブロックされてしまうので、結合したモナド全体で並列実行したい場合には、モナドの合成を行う必要があるためです。このケースが、型クラスArrowとその関連型クラスであるCategoryの最も有効な応用になると思います。

以上のように、現時点では型クラスCategoryとArrowはほぼPromiseモナドの専用品ですが、Lens(scalaz.Lens、Lenses: A Functional Imperative)といった応用が増えてくれば、より活用の範囲が広がるのではないかと思います。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月14日月曜日

Scala Tips / Validation (11) - モナド

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

ここで、Validationを「モナド」と呼んでいますがValidationの場合はちょっと注意が必要なので、その補足記事です。

Scalaのモナド

Scalaの場合、以下のメソッドが文法上特別扱いされています。特別なインタフェースなどを実装しなくても、これらのメソッドさえ定義すればfor式が自動的に解釈するという異例の扱いです。

  • map
  • flatMap
  • filter, withFilter
  • foreach

この中で、for/yield式に使われるのがmapとflatMap、(yieldのない)for式に使われるのがforeachです。filterとwithFilterは両方で使われます。

モナドを「unit演算、bind演算がありモナド則を満たす演算機構を持つオブジェクト」とすると、Scalaの場合は引数一つのコンストラクタがunit演算、flatMapメソッドがbind演算に対応し、これらの演算がモナド則を満たすオブジェクトがモナドということになります。

目安としては、flatMapメソッドが提供されているオブジェクトをモナドとして考えてよいでしょう。(厳密にはモナド則が満たされていることを確認する必要がありますが、これはオブジェクト実装者を信用するということでクリアすることにします。)

for/yield式

for/yield式はモナド演算の文法糖衣ですが、モナド演算に加えてファンクタ演算とフィルタ演算の文法糖衣となっています。

flatMapを提供しているオブジェクトをモナドと呼ぶことにするとすると、同様にmapメソッドが提供されているオブジェクトはファンクタ(関手)と呼ぶことができるでしょう。

フィルタ演算は、モナドやファンクタのような圏論の概念とは直接は関係ないと思いますが、for式で使用することができるようになっています。filterメソッドまたはwithFilterメソッドを提供するオブジェクトはこのフィルタ機能をfor式から使うことができます。なお、withFilterメソッドがfor式向けに最適化されたフィルタで、for式ではwithFilterメソッドがない場合にfilterメソッドを使うようになっています。

for式(yield句なし)

yield句なしのfor式は、手続き型的なループによる制御構造を実現します。副作用を前提とした制御構造なので、圏論的な概念とのつながりはありません。

foreachメソッドを提供したオブジェクトはfor式から使用することができます。

このfor式の場合もフィルタ機能は持っているのでwithFilterメソッドまたはfilterメソッドは有効に機能します。

広義のモナド

Scalaの場合、for式で使えないとモナドとしての利用価値が著しく落ちるので、広い意味では以下の3つのメソッドを提供しているオブジェクトをモナドと考えておくとよいでしょう。もちろん、withFilter/filterが提供されていればより利便性は高くなります。flatMapメソッドを提供しているオブジェクトはたいてい他のメソッドも提供しているので、いずれにしてもflatMapメソッドが目安となります。

  • map
  • flatMap
  • foreach
Validation

Validationは、mapメソッド、flatMapメソッド、foreachメソッドを提供しています。このため、ValidationはScala的にはモナドということになります。

ScalazのMonad

モナドを「unit演算、bind演算がありモナド則を満たす演算機構を持つオブジェクト」とすると、Scalaの場合は演算機構を対象となるオブジェクトのメソッドとして実現していました。一方Scalazの場合は演算機構を(対象となるオブジェクトのメソッドではなく)型クラスのインスタンスとして提供します。

ScalazではMonadという型クラスが提供されています。この型クラスMonadの型クラスインスタンスを持っているオブジェクトが、Scalazにおけるモナドということになります。

ScalazにおけるMonad関連の型クラスの継承関係は以下のようになっています。



型クラスMonadは、Bind&Apply&Functor&Pureとなっていますが、このうちunit演算を型クラスPure、bind演算を型クラスBindが提供しています。

Monadの基本演算であるbind演算は型クラスBindで実現しています。このため型クラスBind&型クラスPureが提供されていれば事実上モナドとして動作することができます。たとえば>>=メソッドを使用することができます。

型クラスMonadは型クラスBind&型クラスPureを引き継いでいるので名実ともにモナドということになります。たとえばfoldLeftMメソッドやreplicateMメソッドなどは型クラスMonadに対応しているので、こういった高度な機能を使う場合は型クラスMonadの型クラスインスタンスが必要になります。

Validation

Scalazの基本設定では、以下の型クラスに対してValidationの型クラスインスタンスが定義されています。

  • Applicative
  • Apply
  • Pointed
  • Functor
  • Pure

一方、MonadとBindの2つの型クラスではValidationの型クラスが定義されていません。(型クラスInvariantFunctorの型クラスインスタンスも提供されていませんが今回の議論では無関係なので無視します。)

このため、型クラスMonadや型クラスBindが対象となる演算に対して、Scalazの基本設定ではValidationは対象外になっています。

つまり、Scalazの中ではValidationはモナドではない、ということです。

整理すると、ValidationはScala的にはモナドである一方、Scalazではモナドではない、ということですね。現象としてはflatMapメソッドは使えfor式にもモナドとして認識されるものの、>>=メソッドやfoldLeftMメソッド、replicateMメソッドなどScalazのモナド向けの機能は使えません。

モナドに関してValidationは、ちょっとイレギュラーな実現方法になっているので注意が必要です。

Validation.Monad

Scalazの基本設定ではValidationはMonadではありませんが、設定をするとMonadとして使用することができます。

具体的には「import Validation.Monad._」のおまじないをすれば、ValidationをScalazのモナドとして使うことができるようになります。

ただし、こうするとValidationをApplicative Functorとして使うときの挙動が変わってくるようです。詳しくは「Scala Tips / Validation (5) - flatMap」のノートを参照してください。

このためValidationをMonadとして使う場合には、以下のように設定をimportするスコープを区切るとよいでしょう。

def f(a: ValidationNEL[Throwable, Int]): ValidationNEL[Throwable, List[Int]] = {
  import Validation.Monad._
  a.replicateM[List](10)
}

replicateMメソッドはMonad内の要素を10回複製したものを、新たに作成したコンテナとして動作するMonoidに格納し、このMonoidをさらにMonadに格納して返すメソッドです。

ここでは、ValidationがMonad、ListがMonoidになります。

以下のように動作します。文章にして書くと難しくみえますが、動作はそれほど難しいものではありません。

scala> val x = 1.successNel[Throwable]
x: scalaz.Scalaz.ValidationNEL[Throwable,Int] = Success(1)

scala> f(x)
res182: scalaz.Scalaz.ValidationNEL[Throwable,List[Int]] = Success(List(1, 1, 1, 1, 1, 1, 1, 1, 1, 1))

このように「import Validation.Monad._」のおまじないをすれば、ValidationをMonadとして使用することができるわけです。

諸元

  • Scala 2.9.2
  • Scalaz 6.0.4

2012年5月11日金曜日

データフローDSL考 (6) - 並行処理

データフローDSLについてのアイデアメモの続きです。データフローDSLでの並行処理についてです。

データフローDSLを考える上で外せない要素が並行処理です。

今後はますます低クロック&メニーコアの方向にスマートデバイス、サーバーともシフトいくことが予想されます。こういったプラットフォーム上でアプリケーションを高速動作させるためには、アプリケーションが、ネイティブな並列処理プログラムである必要があります。外付けで、部分的というのはダメということです。

もちろん、現行のマルチスレッドプログラミングでは、プログラミングの難易度が高くなりすぎて、一般的なエンタープライズアプリケーション開発の文脈でネイティブな並列処理プログラムを書くことは事実上不可能です。

この目的で期待されているのが関数型プログラミングですね。

データフローDSLを設計する際もこの要因を取り込んでいく必要があります。

関数型プログラミングが並行プログラミングの本命であるなら並行処理の記述に独自のセマンティクスを持ち込むより、関数型プログラミングのセマンティクスを自然な形で取り込んでいくのが自然なアプローチになります。

並行プログラミングの2つの形

並行プログラミングを考える上では、以下の2つの異なったユースケースがあることを意識しておく必要があります。

  • 大規模演算を並行処理して実行時間を短縮する
  • 非同期事象を扱う

応用編として、大規模演算を並行処理して実行時間を短縮するしつつ、随時非同期事象を取り込んでいく、というのもありそうですが、話が発散しそうなので、まずは考えないことにします。

並行処理で時間を短縮

並行処理で時間を短縮することを考える上で重要なのは、処理の開始と終了は同期型ということです。処理の内部では、複数のコアを同時に使用した並列処理を行う場合でも、外部からの見かけは同期型となるわけです。

このような処理の場合も、内部実装に並列処理を陽に記述しなければならないのが現行のマルチスレッドプログラミングの問題点です。

この問題の解決に期待できるのが、Monadicプログラミングです。Monadicプログラミングでは、普通のパイプライン的な処理を書くと、内部的に自動的に並行実行され、自動的に同期してくれるような振舞いを実現することができます。プログラマは普通のMonadicプログラミングをするだけで自動的に並列処理を手に入れることができるわけです。

パイプライン的な処理を逸脱した複雑なデータフローはMonadicプログラミングで直接カバーできませんが、サービス・バス的なメカニズムで克服できるのではないかというのが、ここまでの議論でした。モデル記述の場合は、パイプライン+サービス・バスで記述したグラフ構造によるデータフロー・モデル全体を並列処理させるようなアプローチもあります。たとえば、モデルをコンパイルしてAsakusa Frameworkに落とし込めば可能です。

フレームワークAPI DSLの場合は、サービス・バスによる非同期処理の部分で、手作業的な並列プログラミングが出てくることになりそうですが、この部分を極力小さくできれば、実用的に使えるのではないかと思います。

非同期事象

後者の非同期事象は、関数型プログラミングではアクターで扱うのが一般的だと思います。

並列プログラミングでは、非同期事象の扱いも重要な項目ですが、データフローDSLという意味ではスコープ外と考えてよいでしょう。

あえて考えるとすると、データフローDSL的には、パイプラインを合成するためのサービス・バスの部分で接点が出てきそうです。サービス・バスのチャネルに外部事象の入力を認めれば、非同期事象を扱う事が可能になります。

Monadicプログラミング

パイプライン記述でどこまで並列処理を記述できるのかという点が大きな論点になってきます。

そういう観点で考えてみたのが「関数型とデータフロー(5)」です。以下の図では、ListモナドをKleisli圏で合成していますが、こういったアプローチで並列処理をシンプルに記述できないか、ということです。

最近、ちょっと面白そうに思っているのがScalazのPromiseをApplicative演算で使用することで並列処理を記述する方法です。これは、いずれこのブログでも紹介したいと思います。

いずれにしても、Monad、Promise、Kleisli、ApplicativeといったMonadicプログラミングの要素が、並列処理の記述に非常に適していると考えています。

まとめ

今回はデータフローDSLと並列処理の関係について、アイデアレベルでざっくり考えました。

Monadicプログラミングは、普通の処理を簡潔に記述できる点が現時点の逐次プログラミングでも有効ですが、並列プログラミング時代に入ってくると、「Monadicプログラミングなしにはまわっていかない」、といったプログラミングの中心軸になるのではないかというのがボクの予測です。

データフローDSLのパイプライン記述部も、これらの要素を活かしたMonadicプログラミング記述によるアプローチを軸に考えていきたいところです。

2012年5月10日木曜日

データフローDSL考 (5) - アイデアメモ

今回はアイデアメモです。

一連のエントリでデータフローDSLの論点、具体例として私家版Asakusa DSLとg3を見てきました。

  • データフローDSLとして、パイプライン方式が有力
  • パイプライン方式の欠点を補う上でサービス・バス方式が有力

その上で、技術的な論点として以下のものがあります。

  • 静的型付け (フレームワークAPI DSL)
  • 並行プログラミング (フレームワークAPI DSL)
  • ロジックの記述 (モデルDSL)
  • フレームワークAPI DSLの統合

また、新技術の取り込みという課題もあります。

Monadicプログラミング

関数型言語的にデータフローをどう扱うのがよいのかということを調べる目的もあって、ここ半年ほどMonadicプログラミングを調べてきました。

Monadicプログラミングの枠組みでパイプラインを構成するのが関数型的なアプローチということは、ある程度予測はしていましたが、これをデータフロー・モデルの記述に適用する際の細々とした留意事項を、クリアできるのかという点を具体的に把握するのが、調査の目的です。

型クラスを用いることで、ScalaネイティブなMonadicプログラミングを維持しつつデータフロー・モデルに適用する事ができそうなことを確認できました。また、純粋なパイプラインだけではデータフロー・モデルは記述できませんが、これはサービス・バス的なアプローチで克服できそうな目処が立ちました。

この点も含めて、Monadicプログラミングの記述方式がデータフローDSLを構成するパイプラインの記述に直接使用できそうな感触を得ました。

型クラスを使うことで、モデルDSLとフレームワークAPI DSLの統合もできるのではないかという期待も持っています。このアプローチの枠組みで、モデルDSLへのロジックの埋込みを実現できれば理想的です。

Monadicプログラミングの記述力

サービス・バス的アプローチでパイプラインを合成してデータフロー・モデルを記述するという方針を採るので、パイプラインでデータフロー・モデルの全てを記述できる必要はありません。

ただ、そうはいってもパイプラインの範囲内でより複雑な構成を記述できると、データフロー・モデルを記述する際の記述力も上がります。

そういう意味で、Monadicプログラミングでどこまでできるのかが、技術的な関心事となります。

分岐と合流

パイプラインは基本的に直線の単線ですが、Arrowを使って分岐と合流のあるデータフローを記述することができます。(「関数型とデータフロー(2)」)



正常系と異常系

パイプラインにモナドを使用することで、正常系と異常系などの文脈を持ちまわることができます。(「関数型とデータフロー(3)」)




また、この技術の応用編になりますが、フレームワーク側で内部的に行う処理もモナドの裏側(join演算)に隠蔽することもできます。このことでパイプラインの記述力が大幅に向上します。

モナドの合成

クレイスリ圏でモナドを合成することができます。(「関数型とデータフロー(4)」)




パイプラインと部品として用意したモナドを合成して、さらに大きな部品を構築することができます。

ちょっと長くなってきたので、次回に続きます。

2012年5月9日水曜日

データフローDSL考 (4) - g3

データフローDSLという観点で、今までScalaで2つの内部DSLをつくってきました。一つはモデル記述の私家版Asakusa DSL、もう一つはg3向けのフレームワークAPIです。

今回はg3向けのフレームワークAPIについて考えます。

g3については、このブログでも色々書いてきました。詳細は以下のリンクを参照してください。

g3

g3はクラウドアプリケーションのサーバーサイドで動作するサービス部向けのアプリケーションフレームワークです。

元々は、SimpleModelerでクラウドアプリケーションの生成を行う際に、現状では適切なフレームワークが見つからなかったので、「威力偵察」的な意味もあって自分で作ってみることにしたものです。

以下の特徴を備えています。

  • REST指向
  • イベント駆動
  • サービス・バス
  • Atom Publishing指向
  • マルチ・プラットフォーム
  • 運用のスケーラビリティ
  • パイプライン・プログラミング
  • 並行処理
  • HTML生成やRDBMSアクセスをドライバモジュールでモジュール化
  • WebSocket
REST指向、イベント駆動、サービス・バス

クラウドアプリケーション・フレームワークとして、まず欲しかったのはREST指向のイベント駆動&サービス・バスです。

RESTイベントをハンドリングするというより、すべてのイベントをRESTイベントとして抽象化して、この抽象RESTイベントでサービスを駆動します。サービスはサービス・バス上で動作し、チャネルを介して疎結合&非同期に連携します。

エンタープライズの世界ではESB(Enterprise Service Bus)が広く用いられています。ESBはアプリケーションレベルの少し粒度の大きいモジュールをターゲットにしたもので、現状ではXMLを用いて組立て情報を記述するのが一般的です。また、モジュールの連携にはMQを想定しています。EIP(Enterprise Integration Patterns)もこのESB上での構築を想定したものです。

g3の提供するサービス・バスは、ESB的なものをよりシンプルにインメモリ指向で使うことを指向しています。ただし、PaaSと連携することで、クラウドスケールのESBとして使用することもできるようなアーキテクチャにしています。

たとえば、Google AppEngineとの連携は現状でも可能です。Google AppEngineはPaaSの先行事例なので、アーキテクチャの確認の意味もあり、g3では当初から対応しています。

まだ未実装ですが、技術的にはJMS経由でMQを介して他のESBとの連携も可能なはずです。

Atom Publishing指向

抽象REST指向をさらに進めて、抽象Atom Publishing指向の要素を加えています。通常は抽象Atomとしてデータ操作して、必要に応じて抽象度の低い抽象RESTデータを操作することを指向しています。

マルチプラットフォーム&運用のスケーラビリティ

g3はマルチプラットフォームと運用のスケーラビリティも念頭において設計しています。抽象RESTイベントという切り口で、外部イベントと内部処理を疎結合にしているのがポイントです。

  • スタンドアロンコマンドとしても動作
  • Servlet
  • Google AppEngine

g3は、CLIのインタフェースを抽象RESTイベントに変換するフロントエンドを持っているので、スタンドアロンのコマンドでも動作できるようになっています。

g3は、普通のServletとして動作させることができます。このため、TomcatやJava EE環境でそのまま利用できます。

g3は、Google AppEngine上で動作させることもできます。g3は、AppEngineのスケーラビリティを活かすようなアーキテクチャになっています。AppEngineのPaaS機能によって、クラウドスケールでのスケーラビリティを得ることができるようになるはずです。

パイプライン・プログラミング&並行処理

チャネルで抽象RESTイベントが発生しますが、この抽象RESTイベントをパイプライン上上で処理するのがg3の眼目の一つです。狭義では、一連のエントリ「データフローDSL考」で話題にしている項目です。

私家版Asakusa DSLは、モデル記述用とということもあって静的型付けにしていますが、g3はイベント駆動のアプリケーションAPIということもあるので動的型付けにして、パイプラインを上を流れるデータをPartialFunctionでマッチしたもののみ処理を行うプログラミング・モデルにしてみました。このプログラミング・モデルでは、複数種類のイベント(正常系イベントと異常系イベントなど)を同じパイプライン上に流すことが可能になります。

「データフローDSL考」シリーズで説明してきたとおり、単純なパイプラインでは、複雑なデータフロー・モデルを記述することはできません。

g3では、この問題を解決するために、サービス・バスを用います。チャネルから抽象RESTイベントで駆動されたパイプラインは、パイプライン上で加工処理を行いながら、他のチャネルに対して新たな抽象RESTイベントを同期発行または非同期発行していきます。パイプライン上での加工処理が完了後、RESTイベントを同期発行した主体に処理結果を返します。

このように、サービス・バスを経由して他のパイプラインと連携するメカニズムを導入することによってパイプライン・モデルの欠点を克服しています。

サービス・バスによってパイプライン・プログラミング・モデルの問題を解決することを含めて、広義のデータフローDSLと考えることができます。g3では、この広義のデータフローDSLという観点でも、パイプライン・プログラミング・モデルがうまく機能することを確認できました。

問題は、非同期発行した処理を回収するための同期機構です。この同期機構を実現するために、g3の内部で重たい処理をしているのですが、g3のユースケースを勘案した上での全体のバランスからするとちょっと重たすぎた感じがあり、改良ポイントと考えています。

HTML生成やRDBMSアクセスをドライバモジュールでモジュール化&WebSocket

抽象RESTイベント、サービス・バス、パイプライン・プログラミングのメカニズムの上でモジュールによる機能拡張のメカニズムを構築しました。色々な機能をこのセマンティクス&DSL的にうまく埋め込めることが確認できました。

評価

g3は、アプリケーションAPIとしてのデータフローDSLとしては色々チャレンジしてみた項目も含めて、概ねうまくいっているかなというのが自己評価です。ポイントは、パイプライン・プログラミング・モデルとサービス・バスをScala DSLでうまく結び付けることができた点です。

ただ、色々と使ってみて以下の点が問題を認識しています。

  • 非同期同期機構が重たすぎる(前述)
  • 可能であれば静的型付け化したい
  • モデル記述DSL(SimpleModeler)との統合がしたい
新技術

g3を作りはじめた2010年初頭からは、Scalaでも色々な技術革新がありました。g3もこれらの技術を取り込んで、DSLをさらに最適化していく必要があります。

  • 型クラス
  • Monadicプログラミングの進化(Scalaz)
  • Scala DSL技術の進化(Unfiltered など)
  • 並行プログラミングの進化(Akka、Scalaz Promiseなど)
  • マクロ(Scala 2.10予定)
  • Play 2.0

型クラスを用いると、静的型付けのセマンティクスでパイプラインを駆動できるのではないかという期待があります。また、モデル記述DSLとの統合も可能かもしれません。型クラスの登場によって、いろいろな可能性が広がってきました。

Monadicプログラミング、Scala DSL技術、並行プログラミングは地道に進化してきており、2010年年初段階とは別世界の感があります。これらの進化を取り込んで、より強力なプログラミング・モデルを構築する必要があります。

さらに、Scala 2.10ではマクロ機能の導入が予定されています。このマクロはC的なテキスト置換のマクロではなく、ASTレベルでモデル操作ができるものです。これはかなり強力で、DSL技術に大きく寄与することになるでしょう。

プラットフォームとしては、Play 2.0が非常に強力なので、これもターゲットに加えて行きたいところです。

今後の展開

g3も2010年年初段階の技術ベースとしては、いい感じになっていると思いますが、最新技術の視座からみると少し古びてきているようです。

特に、型クラスやMonadicプログラミングの技術を使うことで、より簡潔で強力なプログラミング・モデルを構築できる可能性が高く、この進化を取り込んでいくことが急務です。

並行プログラミングも、フレームワーク内で独自に行うよりもAkkaなどの機構をうまくプログラマが活用できる形に持って行く方が適切です。

また、可能であれば静的型付けやモデルDSLとの統合なども行っていきたいところです。

以上の点から、g3をオーバーホールして、次世代向けのアプリケーション・フレームワークを作ることを考えています。技術的にはScala 2.10のマクロ機構のインパクトが極めて大きそうなので、この技術の評価ができた後に取り組むことになりそうです。

2012年5月8日火曜日

データフローDSL考 (3)

ニコニコ超会議 2012超エンジニアミーティングのScalaユーザーグループのコマで「Scalaでプログラムを作りました」のセッションを行いましたが、右の図はそのスライドで用いたDSLの分類です。

ここでは、DSLを分類する軸として、用途の軸と実現方法の軸を用いています。

用途の軸の項目は以下の2つです。

  • モデル
  • フレームワークAPI

実現方法の軸の項目は以下の2つです。

  • 内部DSL
  • 外部DSL

一口にDSLといっても2×2で4種類のDSLに分類することができるわけです。各象限には、セッションで取り上げた技術を分類して配置しています。

Scalaは、関数型言語の機能と文法上の工夫で内部DSLの実現に適した言語となっています。またパーサーコンビネータを用いることで外部DSLの実現も簡単に行うことができます。

Scalaの名前の由来である「Scalable Language」は、言語をドメイン向けに拡張していくことができる、ということを意味していると思いますが、そういう意味で、まさにDSL指向言語ということができるでしょう。

データフローDSLという観点で、今までScalaで2つの内部DSLをつくってきました。一つはモデル記述の私家版Asakusa DSL、もう一つはg3向けのフレームワークAPIです。

まず、データフローDSLのモデル記述の実現例である私家版Asakusa DSLについてみていきます。

私家版Asakusa DSL

Asakusa Framework(以下Asakusa)は、基幹業務システムのバッチを高速処理するためのHadoopフレームワークです。単にHadoopを使いやすくしているだけでなく、ジョブ管理などを含めた基幹業務に必要なミドルウェアの基盤を提供している点が特徴です。

Hadoopのベースになった技術はMapReduceですが、mapとreduceという用語からも分かるとおり、関数型的なデータフロー演算が、計算モデルのベースになっています。オブジェクト指向と関数型の接点はデータフローになりそうだというお話をしてきましたが、クラウド・プラットフォームと基幹業務の接点もデータフローということになりそうです。

オブジェクト指向、関数型といった開発手法の方向からも、プラットフォームや業務ドメインからの方向からも、データフローに収斂する構図になっており、データフローは一種のホットスポットとなっています。

そういう意味でも、データフローDSLの果たす役割は今後大きくなりそうです。その一つの応用として、Asakusa向けのScala DSLは格好の例題となります。

Scala DSL

私家版Asakusa DSL(以下Scala DSL)は、現在Javaベースで提供されているAsakusa DSLのScala版の試作品です。去年の3月頃に作成しました。(「Asakusa Scala DSL」)

Scala DSLで記述したモデルは以下のようになります。

package sample

import org.simplemodeling.dsl.domain._
import org.simplemodeling.dsl.flow._

class 仕入明細データ extends DomainResource
class 仕入返品データ extends DomainResource
class 費用振替データ extends DomainResource
class 売価変更データ extends DomainResource

class 修正在庫振替TRN extends DataSet
class 修正未収収益TRN extends DataSet
class 修正在庫移動TRN extends DataSet
class 未払計上TRN extends DataSet

class 仕入TRN extends DataSet
class 在庫振替TRN extends DataSet
class 在庫移動TRN extends DataSet
class 未収収益TRN extends DataSet

class 計上済仕入TRN extends DataSet
class 計上済未収収益TRN extends DataSet
class 計上済未払費用TRN extends DataSet
class 更新済買掛残高TRN extends DataSet

class 請求エラーTRN extends DataSet
class 支払不可消込TRN extends DataSet
class 支払可消込TRN extends DataSet
class 照合済支払費用TRN extends DataSet
class 照合済未収収益TRN extends DataSet
class 照合済仕入TRN extends DataSet
class 照合済請求TRN extends DataSet

class 仕入データ extends DataSource4[仕入明細データ, 仕入返品データ,
                                     費用振替データ, 売価変更データ]
class 修正データ extends DataSource4[修正在庫振替TRN, 修正未収収益TRN,
                                     修正在庫移動TRN, 未払計上TRN]
class 売価変更在庫変更TRN extends DataSet
class 仕入データTRN extends DataSource4[仕入TRN, 在庫振替TRN,
                                        在庫移動TRN, 未収収益TRN]
class 残高更新TRN extends DataSource4[計上済仕入TRN, 計上済未収収益TRN,
                                      計上済未払費用TRN, 更新済買掛残高TRN]
class 請求TRN extends DataSet
class 会計データTRN extends DataSource7[請求エラーTRN, 支払不可消込TRN, 支払可消込TRN,
                                        照合済支払費用TRN, 照合済未収収益TRN,
                                        照合済仕入TRN, 照合済請求TRN]

case class 仕入データ取り込み(cout: Port[売価変更在庫変更TRN]) extends Operator12[仕入データ, 仕入データTRN, 売価変更在庫変更TRN](cout)
case class 残高更新(cin: Port[修正データ]) extends Operator21[仕入データTRN, 修正データ, 残高更新TRN](cin)
case class 照合処理(cin: Port[請求TRN]) extends Operator21[残高更新TRN, 請求TRN, 会計データTRN](cin)

// サブフロー用に追加したオペレーション
case object 売価変更在庫変更TRN修正 extends Operator11[売価変更在庫変更TRN, 売価変更在庫変更TRN]

// サブフロー用に追加したオペレーション
case object 修正データ追加修正 extends Operator11[修正データ, 修正データ]

// 図7 改善された会計処理バッチの処理フロー
case class 会計処理バッチ extends Flow32[仕入データ, 修正データ, 請求TRN,
                                    会計データTRN, 売価変更在庫変更TRN] {
// 追加したサブフロー1
// 処理結果を会計処理バッチの2番目の出力ポートに出力                                      
  val 売価変更在庫変更TRN補正 = new Flow11[売価変更在庫変更TRN, 売価変更在庫変更TRN] {
    start op11(売価変更在庫変更TRN修正) end(会計処理バッチ.this.out2)
  }

// 追加したサブフロー2
// 会計処理バッチの2番目の入力ポートからデータを入力
  val 修正データ補正 = new Flow11[修正データ, 修正データ] {
    start(会計処理バッチ.this.in2) op11(修正データ追加修正) end
  }

// 仕入データ取り込みの2番目の出力先をサブフロー1に変更
// 残高更新の2番目の入力元をサブフロー2に変更
  start op12(仕入データ取り込み(売価変更在庫変更TRN補正.post1)) op21(残高更新(修正データ補正.get1)) op21(照合処理(in3)) end
}

Scala DSLは(仮に)SimpleModelerに組み込んで、データフロー図を表示できるようにしています。このモデルからSimpleModelerで生成したデータフロー図は以下のものになります。


このデータフローDSLは、静的型付けによってデータの入出力関係やパラメタの個数の間違いをエラーチェックできることが特徴です。このこともあり、入出力のデータの定義を詳細に行なっています。

しかし、メインとなるデータフローの記述は以下の一行に収まっています。op12は入力が1つ、出力が2つあるプロセス、op21は入力が2つ、出力が1つのプロセスです。大枠は「仕入データ取り込み」を行った後「残高更新」する処理となります。

「仕入データ取り込み」はメインの出力に加えて「売価変更在庫変更TRN補正」サブフローに出力を行います。また、「残高更新」はメインの入力に加えて「修正データ補正」サブフローからの入力を行います。

start op12(仕入データ取り込み(売価変更在庫変更TRN補正.post1)) op21(残高更新(修正データ補正.get1)) op21(照合処理(in3)) end

データフローを構成するサブフローは以下の「売価変更在庫変更TRN補正」と「修正データ補正」の2つです。それぞれのサブフローも一行に収まっています。

val 売価変更在庫変更TRN補正 = new Flow11[売価変更在庫変更TRN, 売価変更在庫変更TRN] {
    start op11(売価変更在庫変更TRN修正) end(会計処理バッチ.this.out2)
  }
val 修正データ補正 = new Flow11[修正データ, 修正データ] {
    start(会計処理バッチ.this.in2) op11(修正データ追加修正) end
  }
評価

Scalaの内部DSLでデータフローDSLを設計する上での論点としては以下のものが挙げられます。

  • パイプラインの記法を取り入れるか否か(ノード記述方式を採用するか否か)
  • 静的型付けをどこまで使うか
  • 2つのフローの接続方式
  • ロジック記述方式

私家版Asakusa DSLは、実験的な意味もあり、パイプライン記法と静的型付けの方に大きく倒した方式にしてみました。そこそこ、うまくいっているのではないかというのが自己評価で、パイプライン記法と静的型付けの組合せは一応実用化の目処がついたと判断しています。

「2つのフローの接続方法」は、サブフローを直接指定する方式にしてみましたが、今の目で見ると、チャネル方式などを導入してサブフロー間を疎結合にしていくアプローチの方がよさそうです。

「ロジック記述方式」は、今回の試作では入れませんでしたが、非常に重要な項目です。内部DSLを採用するメリットの一つがロジックをホスト言語で直接記述できる点にあります。

実用化

実用化に際しては、サブフローを集めた部品を、他の部品と合成してさらに大きくしていく開発手法が求められるようになると思います。サブフロー部品の汎用部品化も視野に入ってきそうです。サブフローの接続方式はチャネル方式の方がこういった開発手法に適していそうなので、改良候補となります。

静的型付け&ジェネリック型&関数型は、こういった部品の合成時のメカニズムおよび合成時のエラーチェックにも威力を発揮しそうです。そういう意味でも、直接ロジック記述することができるメリットも含めて、Scalaをホスト言語にした内部DSL方式は、非常に有力な方式ですね。

SimpleModeler

私家版Asakusa DSLは、仮にSimpleModelerに組み込んでみましたが、Asakusaに限らず汎用的なデータフローDSLとして利用できそうな感触を持っています。

懸案事項はサブフローの接続方式とロジック記述方式です。

サブフローの接続方式はチャネル方式にするとして、問題はロジック記述方式です。これは、型クラスを用いて実現できそうな気がしているのですが、このあたりが次のチャレンジとなります。

折を見てこれらの改良を施した上で、Asakusaはもちろん、他のプラットフォーム向けのコード生成にもトライしてみたいと考えています。