ラベル ofp の投稿を表示しています。 すべての投稿を表示
ラベル ofp の投稿を表示しています。 すべての投稿を表示

2016年12月9日金曜日

ReaderWriterStateモナドと畳込み

ReaderWriterStateモナドは「Patterns in Types - A look at Reader, Writer and State in Scala」を見てからずっと気になっていたのですが、実務のプログラミングでも汎用的な基盤として使えるのではないかとふたたび自分の中でブームになってきたので、少し試してみました。

例題

レコードを正常なものと異常なものに選別する処理を考えます。異常なレコードは異常と判断した理由つきで記録します。

データ連携処理ではよく出てくる処理だと思います。

この処理を以下の関数として実装することにします。

def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord])

この関数を以下のバリエーションで実装していきます。

Foldable
通常の畳込み
Monoid
モノイドによる畳込み
State
Stateモナドによる畳込み
Traverse&State
TraverseとStateモナドによる畳込み
Reducer
Reducerを使った畳込み
ReaderWriterState
ReaderWriterStateモナドを使った畳込み

直接の目的はボクが常用しているFoldableによる畳込みとReaderWriterStateモナドによる畳込みを比較することです。

同時に色々なアプローチの比較も行い、それぞれのアプローチの使い所を探っていきます。

準備

まずプログラムが扱うドメインのオブジェクトを定義します。

object Domain {
  type Record = Map[String, Any]
  type Errors = Vector[ErrorRecord]
  type Reason = String
  case class ErrorRecord(record: Record, reason: Reason)
  case class RecordsState(totalCount: Int = 0, errorCount: Int = 0) {
    def success = copy(totalCount = totalCount + 1)
    def error = RecordsState(totalCount + 1, errorCount + 1)
  }
  trait ExecutionContext {
    def verify(r: Record): Option[Reason]
  }
  class DefaultExecutionContext() extends ExecutionContext {
    def verify(r: Record): Option[Reason] =
      if (r.get("id").isEmpty)
        Some("No id")
      else
        None
  }
}

以下の型、case class、クラスを定義しました。

Record
レコード
ErrorRecord
エラーとなったレコードと理由
Reason
エラー理由
Errors
ErrorRecordの集まり
RecordState
処理状況を記録
ExecutionContext
実行コンテキストのインタフェース
ExecutionContextImpl
実行コンテキストの実装

単にエラーコードを選り分けるだけでなく、以下の機能を実現できるようにしています。

  • 実行状況をRecordStateに記録して取得可能にしている。
  • エラーの判定のロジックをExecutionContextとして指定可能にしている。

Foldable

FP(Functional Programming)で一般的な畳込み処理です。

object FoldLeft {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) =
    xs.foldLeft(Z(context))(_+_).result
}

VectorのfoldLeft関数を使って畳込み処理を行います。

case classを使う手法はボクが個人的に使っているもので一般的ではないと思いますが、特に難しくはないと思います。(「foldの小技」)

FoldLeftのfold関数はExecutionContextを暗黙パラメタとして受取りcase class Zのパラメタとして渡しています。case class ZはこのExecutionContextを使用してロジックを実行します。ロジックの可変部分をExecutionContextに分離することでfold関数の処理をチューニング可能な構造になっています。

Monoid

次はMonoidを使った畳込みを考えてみます。

object FoldableMonoid {
  import Domain._
  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    val context: ExecutionContext = new ExecutionContextImpl()
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def +(rhs: Z): Z = copy(
      records = records ++ rhs.records,
      errors = errors ++ rhs.errors
    )
  }

  object Z {
    def empty = Z()
    def point(rec: Record) = empty + rec
  }

  implicit object ZMonoid extends Monoid[Z] {
    def zero = Z.empty
    def append(lhs: Z, rhs: => Z) = lhs + rhs
  }

  def fold(xs: Vector[Record]): (Vector[Record], Vector[ErrorRecord]) = {
    xs.foldMap(Z.point).result
  }
}

モノイドの場合、実行コンテキストの意図のExecutionContextを外部から与えることは筋悪そうなので固定のものを使うことにしています。

評価

コーディング的には、Monoid計算に適合するように各種関数を用意したり、型クラスMoonoidの型クラスインスタンを定義したりという手間がかかります。

何回も使用するロジックの場合はよいですが、その場限りのロジックの場合はコーディングのオーバーヘッドの方が大きくなるのでFoldable方式の方がよさそうです。

またExecutionContextを外付けで与えることができないのはかなり大きな問題です。

Monoidについては、すでにMonoidがある場合はそれを利用するのがよいですが、畳込みのために、わざわざMonoidのメカニズムを積極的に使うというほどではないようです。

State

次はStateモナドを使った畳み込みです。

object StateWithTraverse {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    val ts = xs.traverseS(x => State[Z, Unit] {
      case s => (s + x, ())
    })
    ts.exec(Z(context)).result
  }
}

scalazの型クラスTraverseにはStateモナドを使って走査する機能があります。Stateモナドで畳み込み動作をするようにしておけば、Traverseでの走査の過程で畳み込みを行うことができます。

ここではFoldableで使用したcase class Zと同じものをStateモナドでの状態として使用する実装を行っています。

実行コンテキストであるExecutionContextは状態の一部として受渡しています。

評価

Stateモナドの使い方に慣れていれば、Foldableとほぼ同じような手間でプログラミングすることができます。

ただ、Stateモナド実行のオーバヘッドなどを考えるとFoldableで間に合っているものをわざわざStateモナド化する必然性はなさそうです。

再利用可能なStateモナド部品を作った時に、このロジックで畳み込みに利用することも可能という選択肢として考えておくとよいと思います。

Stateモナドは畳込みの汎用ロジック向けではなく、以下の記事にまとめたように状態遷移/状態機械を作る時のキーパーツとして考えていくのがよさそうに思いました。

Traverse&State

Stateモナドは、1つの処理ごとに型パラメータAで示す処理結果を出力する機能があり、for式などで組み合わせる時にパラメタとして受け渡しすることで、全体として複雑な処理を記述できる機能を持っているのですが、traverseSによる畳込みの場合はここの部分で、走査結果を蓄積する形になります。

このためTraverseとStateを組み合わせる場合、Traverseの機能を活用してStateの実行結果をTraverse側に蓄積させることができるので、その点を活かした実装に改良してみました。

object TraverseState {
  import Domain._
  case class Z(
    context: ExecutionContext,
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def apply(rhs: Record): (Z, Option[Record]) = context.verify(rhs) match {
      case Some(reason) => (copy(errors = errors :+ ErrorRecord(rhs, reason)), None)
      case None => (copy(records = records :+ rhs), Some(rhs))
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    val ts = xs.traverseS(x => State[Z, Option[Record]] {
      case s => s(x)
    })
    val (s, records) = ts.run(Z(context))
    (records.flatten, s.errors)
  }
}
評価

前節「State」は正常レコードもcase class Z経由で取得することを前提にTraverseの主ルートには「()」を渡していて、事実上封印していました。

ここでは、正常レコードをTraverseの主ルートで受け渡すことができるようにcase class Zにapplyメソッドを追加しました。

case class Zが再利用可能な汎用ロジックを実装できるのであれば、ひと手間かけてapplyメソッドを追加しておくことで、利用範囲が広がります。

ただ、foldLeftよりはやや手間がかかるのは「State」と同じなので、一度限りのロジックに対して普段使いで適用する感じではなさそうです。

Reducer

ちょっと脱線してReducerを使った畳込みを考えてみました。

Reducerは畳み込み処理の中のデータを足し込む処理を汎用化したものです。畳込み対象がMonoidでなく、畳込み結果がMonoidである場合に使用できます。畳み込み処理の走査処理を汎用化(左畳み込み、右畳み込みの最適選択)したGeneratorと組み合わせて使用するのが基本的な使い方のようです。

object Reducer {
  import Domain._
  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    val context: ExecutionContext = new ExecutionContextImpl()
    def result = (records, errors)

    def +(rhs: Record): Z = context.verify(rhs) match {
      case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
      case None => copy(records = records :+ rhs)
    }

    def +(rhs: Z): Z = copy(
      records = records ++ rhs.records,
      errors = errors ++ rhs.errors
    )
  }

  object Z {
    def empty = Z()
    def point(rec: Record) = empty + rec
  }

  implicit object ZMonoid extends Monoid[Z] {
    def zero = Z()
    def append(lhs: Z, rhs: => Z) = lhs + rhs
  }

  def fold(xs: Vector[Record]): (Vector[Record], Vector[ErrorRecord]) = {
    val reducer = UnitReducer((x: Record) => Z.point(x))
    val G = Generator.FoldlGenerator[Vector]
    G.reduce(reducer, xs).result
  }
}

ReducerはMonoid以外の畳込み対象を一度Monoidに変換してから畳み込むというロジックなので、畳込みがMonoidの機能範囲に限定されます。

今回のケースではExecutionContextを外付けにするのが難しいので、Monoidであるcase class Zが内部で固定で持っています。

評価

ReducerはMonoid以外の要素の列をMonoidに畳み込む時のアダプタ的な機能と考えると分かりやすいと思います。ただ、このための機能としてはFoldableのfoldMapコンビネータという非常に汎用的な機能があるので、Reducerをわざわざ使うシーンはあまりなさそうです。

また、色々と糊コードを書かないといけないのとMonoidの制約が入ってくるので、汎用の畳込み機能として使うのはお得ではなさそうということも確認できました。

ReducerはscalazのreduceUnordered関数で並列実行したTaskの結果の順不同畳込みに使用されています。こういった、特別な用途向けの機能と考えておくとよさそうです。

ReaderWriterState

それでは本命のReaderWriterStateモナドを使ってみます。

object ReaderWriterStateFold {
  import scala.language.higherKinds
  import Domain._

  def run[C[_]: Foldable, X, R, W: Monoid, S, A: Monoid](xs: C[X], rws: X => ReaderWriterState[R, W, S, A], r: R, s: S): (W, A, S) = {
    case class RWSZ(
      writer: W = Monoid[W].zero,
      outcome: A = Monoid[A].zero,
      state: S = s
    ) {
      def result = (writer, outcome, state)
      def apply(r: R, x: X) = {
        val (rw, ra, rs) = rws(x).run(r, state)
        RWSZ(rw, ra, rs)
      }
    }
    xs.foldLeft(RWSZ())(_.apply(r, _)).result
  }

  case class Z(
    records: Vector[Record] = Vector.empty,
    errors: Vector[ErrorRecord] = Vector.empty
  ) {
    def result = (records, errors)

    def apply(context: ExecutionContext, rhs: Record) = {
      val z = context.verify(rhs) match {
        case Some(reason) => copy(errors = errors :+ ErrorRecord(rhs, reason))
        case None => copy(records = records :+ rhs)
      }
      (z.errors, z.records, z)
    }
  }

  def fold(
    xs: Vector[Record]
  )(implicit context: ExecutionContext): (Vector[Record], Vector[ErrorRecord]) = {
    def rws(a: Record) = ReaderWriterState[ExecutionContext, Vector[ErrorRecord], Z, Vector[Record]] {
      case (r, s) => s.apply(r, a)
    }
    val (errors, records, z) = run(xs, rws, context, Z())
    (records, errors)
  }
}

run関数は汎用関数なので、今回の用途向けに作成した部分はcase class Zとfold関数だけなのでそれほど大きくはありません。run関数を再利用することを前提にすると、ほとんどStateやTraverse&Stateと同じ手間で畳み込み処理を書くことができます。

ReaderWriterStateモナドは、Stateモナドの持つStateモナド自身と処理結果の出力に加えて実行コンテキストなどの参照専用データの受け渡し(Reader)とログ的な蓄積データの出力(Writer)の機能を持っています。

run関数では、引数に処理対象のVectorとReaderWriterStateモナド、実行コンテキストのExecutionContextと状態を持つcase class Zの初期値を渡しています。実行コンテキストと状態の初期値を外部から与えることができるので、ReaderWriterStateモナドの振る舞いを実行時にカスタマイズできる構造になっています。

run関数の返却値はStateモナドの実行結果の正常レコードとWriterに蓄積されたエラーコード、そしてState(case class Z)の最終結果です。

評価

run関数を事前に用意しておけば、Traverse&Stateモナドとほとんど変わらない使い勝手で使うことができることが確認できました。

Stateモナドの場合は、実行コンテキスト(ExecutionContext)の指定と蓄積データ(エラーレコード)の取得を状態(case class Z)の中に自分で実装する必要がありました。

一方、ReaderWriterStateモナドでは、実行コンテキストの指定は蓄積データの取得はReaderWriterStateモナドの機能としてもっているので、状態と計算結果に実装上の注意を集中することができます。また、実行コンテキストと蓄積データのインタフェースが決まっているので、部品として組み合わせることも可能になります。

本例でもそうであったように、多くの用途ではReaderWriterStateモナドが提供する機能で要件が満たせる事が多いのではないかと思います。そうであるならば、ReaderWriterStateモナドが提供する汎用機能を使って再利用可能な部品を作ることで、部品の再利用を促進できる可能性が高いと考えられます。

考察

畳み込み処理に関しては、一度限りのロジックであるならばFoldableのfoldLeftを使って普通に書くのが一番開発効率がよさそうです。

一方、StateモナドやReaderWriterStateモナドを使って畳込みを実装するのも、それほどの手間でないことも確認できました。StateモナドやReaderWriterStateモナドにピッタリ合うケースでは、一度限りののロジックでもこれらのモナドを使うのもありそうです。

StateモナドやReaderWriterStateモナドは共通部品向けの汎用インタフェースという意味合いが大きいですが、使い方が難しいと積極的には使いづらいところです。どちらのモナドもわりと簡単に使えることが分かったのが収穫でした。StateモナドやReaderWriterStateモナドをつかって再利用可能な部品を整備していく方向性が実現可能という感触を得ることができました。

また、Stateモナド、ReaderWriterStateモナド、Monoid、Reducerの機能差も改めて確認することができました。

当面は以下のような方針で適用していきたいと考えています。

  • 一度限りのロジックはfoldLeft(普通の畳込み)
  • 再利用可能な処理で実行コンテキストがなくMonoid化できるものはMonoid
  • 共通部品はReaderWriterStateモナド化を考える(実行コンテキスト&蓄積データ&計算結果&状態)
  • 必要に応じてStateモナドやReducerを使う

諸元

  • Scala 2.11.7
  • Scalaz 7.2.0

2016年11月30日水曜日

よこはまクラウド勉強会: OFP & OFAD Deep Dive with Reactive Streams

Qcon Tokyo 2016で「オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える」と題してOFAD(Object-Functional Analysis and Design)についてお話させていただきました。

テーマはOFADですが、OFADの前提としてモダンなFP(Functional Programming)を前提としたOFP(Object-Functional Programming)の知識、さらに基本的なOOADの知識が必要なので、非常に広範囲の内容を圧縮して詰め込んだ形になってしまいました。

そこで、もう少し時間を取って説明することができればよいと思っていたのですが、横浜クラウド勉強会 で機会を頂くことができました。

QCon Tokyoでは50分に収めましたが、こちらの方は少し脱線しながら2時間程度のセッションになりました。その後、Reactive Streamsのハンズオンという構成です。

Reactive Streamsの立ち位置

QCon向けの資料をまとめていて感じたのはReactive StreamsがOFP(Object-Functional Programming)のキーテクノロジーではないか、ということです。

Reactive Streamsは以下の2つの方向性があると考えています。

  • 即応性、スケーラビリティのためのメカニズム
  • FP(Functional Programming)の適用範囲を広げる

ここでのFPは純粋関数型プログラミングを意図しています。

前者は今後のクラウドアプリケーションの方向としては当然の方向性です。この方向性を追求する場合には、FPの純粋性を犠牲にしても実行時性能やバックプレッシャーなどのプロトコル拡張を追求することになります。

一方、後者はFPの純粋性は守りながら、FPを外部入出力や大規模データ処理、ストリーミング処理に適用することを目指すものです。

Reactive Streamsは即応性やスケーラビリティという点で注目されていますが、ボクの関心事はそのことよりもFPの適用範囲を広げることに大きく寄与するのではないかという点です。ボクは現時点では後者を重視しているので、(Akka Streamsではなく)scalazと組み合わせて使えるscalaz-streamを愛用しています。

セッションでもお話したOFPのコツはつまるところOOP(Object-Oriented Programming)とFPを適材適所で、ということですが整理すると以下の方針になります。

  • アルゴリズム系の副作用を伴わない処理はFP
  • コンポーネントのファサード部(API/SPI)はOOP
  • OOADのモデリングの実現部はOOP
  • その他はできる限りFP

「できる限りFP」にしたい理由は以下のものです。

  • バグの発生率が圧倒的に少ない。
  • アルゴリズムを効率よく記述できる。
  • 並列/並行/分散処理時代への助走。
  • 将来の証明プログラミングへの備え。

現時点での最大の魅力は「バグの発生率が圧倒的に少ない」がリファクタリングに大きく寄与する点です。長期間持続的に開発を続けるシステムはこの理由だけでOFPを採用する価値があると思います。そして、長期的には「並列/並行/分散処理時代への助走」、「将来の証明プログラミングへの備え」という観点から必須のプログラミング・スタイルになるとすると、先取りして取り入れておきたいところです。

ここで問題となるのは「その他はできる限りFP」です。

FPには外部入出力処理や状態を持った処理の記述が大変という問題があります。スライドに書いたように、モナドの登場で外部入出力処理や状態を持った処理の記述が「困難」から「可能」になったのは大きな前進ですが、OFPの観点からはかならずしも「便利」とは言えないと思います。またScalaの特殊事情として文法的な制約でHaskell程簡明には書けないということもあります。

「できる限りFP」とはいえ便利でないものは使わなくなるのが道理で、対策をとらないと結局OOPの部分が大きく残ってしまう事になってしまいます。

Reactive Streamsがキーテクノロジーではないか、という期待はまさにこの問題の解消に大きく寄与するのではないかという点です。

scalaz-streamが提供するProcessモナドも一種のモナドですが、概念的に分かりやすいのとリソース管理やフロー制御という伝統的なOOPによる外部入出力でも難しかった問題が解決されています。実際に製品開発で使ってみてこの便利さを体感しました。

このような経験からReactive Streamを開発の基盤とすることで、OFPの多くの部分をFP側に倒すことが可能になるのではないかと期待しているわけです。

Reactive Streamsハンズオン

そんな思いもあり、セッションの後半はReactive Streamsのハンズオンにしました。以下にソースコードがあります。

このハンズオンのソースはセッションの朝に急ごしらえで作ったものなので、動作確認などはあまりできておらず、当日会場で調整しながら使用したものです。その点はご留意下さい。

ここでの趣旨は、(即応性、スケーラビリティではなく)FP成分を重視したReactive Streamsについて、プログラミングレベルでのイメージをつかんでいただくことです。

以下簡単に説明します。

ステップ1: Hello World

ステップ1はscalaz-streamのHello World的なプログラムです。scalaz-streamの最小限の使い方を体験することが目的です。

mainメソッドの第1引数に入力ファイル名を、第2引数に出力ファイル名を指定すると入力ファイルを出力ファイルに複写するプログラムです。

ハンズオンの問題は以下になります。

package handson.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step1 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val t = converter(in, out)
    t.run
  }

  def converter(in: String, out: String): Task[Unit] = ???

  def converterProcess(in: String, out: String): Process[Task, Unit] = ???
}

converter関数とconverterProcess関数を実装します。

実装例

ステップ1の実装例です。

package answer.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step1 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val t = converter(in, out)
    t.run
  }

  def converter(in: String, out: String): Task[Unit] =
    converterProcess(in, out).run

  def converterProcess(in: String, out: String): Process[Task, Unit] =
    io.linesR(in).pipe(text.utf8Encode).to(io.fileChunkW(out))
}

converter関数はconverterProcess関数から返されたProcessモナドをrunしてTaskモナドにします。

converterProcess関数はscalaz-streamのパイプラインをProcessモナドという形で構築して返します。

ステップ2: Monadic API

ステップ1はscalaz-streamの最小限の使い方でした。

ステップ2ではscalaz-streamのパイプラインが、通常のMonadic APIとして使えることを体験します。ここでいうMonadic APIはJavaでいう所のStream APIで、FunctorやMonadによるパイプラインをベースにしたAPIです。(Streamは色々な意味付けがされていてミスリーディングな面もあるのでここでは本ブログではMonadic APIと呼んでいます。)

package handson.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step2 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val player = args(2)
    val t = converter(in, out, player)
    t.run
  }

  def converter(in: String, out: String, player: String): Task[Unit] =
    converterProcess(in, out, player).run

  def converterProcess(in: String, out: String, player: String): Process[Task, Unit] =
    io.linesR(in).
      map(toRecord).filter(isPlayer(player)).map(toYear).map(_ + "\n").
      pipe(text.utf8Encode).to(io.fileChunkW(out))

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def isPlayer(player: String)(record: Vector[String]): Boolean = ???

  def toYear(record: Vector[String]): String = ???
}

ステップ1との違いは以下の3つの関数をmapコンビネータ、filterコンビネータでパイプラインに組み込んでいることです。

  • toRecord関数
  • isPlayer関数
  • toYear関数

これらの関数が通常の関数であること、そして簡単にProcessモナドのパイプラインに組み込むことができる点を体験するのが目的です。

実装例

ステップ2の実装例です。

package answer.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step2 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val player = args(2)
    val t = converter(in, out, player)
    t.run
  }

  def converter(in: String, out: String, player: String): Task[Unit] =
    converterProcess(in, out, player).run

  def converterProcess(in: String, out: String, player: String): Process[Task, Unit] =
    io.linesR(in).
      map(toRecord).filter(isPlayer(player)).map(toYear).map(_ + "\n").
      pipe(text.utf8Encode).to(io.fileChunkW(out))

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def isPlayer(player: String)(record: Vector[String]): Boolean =
    record.lift(0) == Some(player)

  def toYear(record: Vector[String]): String =
    record.lift(1) getOrElse "Unknown"
}

toRecord関数、isPlayer関数、toYear関数はごく普通の関数です。これらを簡単にProcessモナドのパイプラインに組み込んで動作することが確認できました。

ステップ3: フロー制御

ステップ3は一種のフロー制御であるチャンク化の体験です。

通常のMonadic APIは構造上フロー制御を行うことができませんが、Processモナドではpipeコンビネータなどの仕組みによってフロー制御を可能にしています。

ここが通常のMonadic APIに対するReactive Steramsの優位点で、外部入出力を効率的に処理することを可能にする拡張となっています。

package handson.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step3 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val t = converter(in, out)
    t.run
  }

  def converter(in: String, out: String): Task[Unit] =
    converterProcess(in, out).run

  def converterProcess(in: String, out: String): Process[Task, Unit] =
    io.linesR(in).
      map(toRecord).
      chunk(1000).map(groupByYear).pipe(process1.unchunk).
      map(toYear).
      pipe(text.utf8Encode).to(io.fileChunkW(out))

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def toYear(record: Vector[String]): String = ???

  def groupByYear(records: Vector[Vector[String]]): Vector[Vector[String]] = ???
}

注目するポイントはパイプライン中の「chunk(1000).map(groupByYear).pipe(process1.unchunk)」の部分です。パイプライン中のchunk関数とpipeコンビネータで適用したprocess.unchunk関数によって、パイプラインを流れるデータを1000個単位でチャンク化しています。

mapコンビネータで指定されているgroupByYear関数はチャンク化したデータに対する処理を行うようになっています。

チャンク化は入出力処理で性能向上するための必須処理なので、これを自動的に行なってくれる部品が提供されていることは生産性に大きく寄与します。

実装例

ステップ3の実装例です。

package answer.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._

object Step3 {
  def main(args: Array[String]) {
    val in = args(0)
    val out = args(1)
    val t = converter(in, out)
    t.run
  }

  def converter(in: String, out: String): Task[Unit] =
    converterProcess(in, out).run

  def converterProcess(in: String, out: String): Process[Task, Unit] =
    io.linesR(in).
      map(toRecord).
      chunk(1000).map(groupByYear).pipe(process1.unchunk).
      map(toYear).
      pipe(text.utf8Encode).to(io.fileChunkW(out))

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def toYear(record: Vector[String]): String =
    record.lift(1) getOrElse "Unknown"

  def groupByYear(records: Vector[Vector[String]]): Vector[Vector[String]] = {
    case class Z(years: Set[String] = Set.empty, result: Vector[Vector[String]] = Vector.empty) {
      def +(rhs: Vector[String]) =
        rhs.lift(1).fold(this) { year =>
          if (years.contains(year))
            this
          else
            Z(years + year, result :+ rhs)
        }
    }
    records.foldLeft(Z())(_ + _).result
  }
}

toYear関数、groupByYear関数を普通に実装するだけです。データをチャンク化する処理はscalaz-streamが提供している部品が自動的に行なってくれます。

フロー制御の例としては以下の記事が参考になると思います。

ステップ4: ストリーム

ステップ4はReactive Streamsでストリーム処理を行う課題です。

まず準備としてストリームを生成する部品EventProcessorを作ります。

package handson.reactive
  
import scalaz.concurrent.Task
import scalaz.stream._  

object EventProcessor {
  val q = async.unboundedQueue[String]

  val eventStream: Process[Task, String] = q.dequeue
}

buildメソッドはscalaz-streamによるパイプラインを作って、これをバックグラウンドで起動します。

最後に呼んでいるstimulusメソッドはEventProcessorを使って入力ファイルの内容を1行づつ読み込み1秒のインターバルでストリームに送出します。

package handson.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._
import scala.concurrent.Future  
import scala.concurrent.ExecutionContext.Implicits.global
import scalax.io._
import scalax.io.JavaConverters._
import java.io.File

object Step4 {
  def main(args: Array[String]) {
    val in = args(0)

    val stream = EventProcessor.eventStream
    build(stream)
    stimulus(in)
  }

  def build(stream: Process[Task, String]) {
    Future {
      val t = converterProcess(stream).to(io.printLines(System.out))
      t.run.run
    }  
  }  

  def converterProcess(source: Process[Task, String]): Process[Task, String] =
    source.map(toRecord).map(toYear)

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def toYear(record: Vector[String]): String = ???

  def stimulus(in: String) {
    val queue = EventProcessor.q
    val input = new File(in).asInput
    input.lines() foreach { line =>
      queue.enqueueOne(line).run
      Thread.sleep(1000)
    }
  }
}

scalaz-streamのパイプラインはconvertProcess関数で作成します。このパイプラインにはmapコンビネータでtoYear関数を接続しています。

本課題ではこのtoYear関数を実装します。

toYear関数は通常の関数です。この通常の関数をscalaz-streamのパイプラインに組み込むだけでFPによるストリーム処理が記述できるのを体験するのが本課題の趣旨です。

実装例

ステップ4の実装例です。

package answer.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._
import scala.concurrent.Future  
import scala.concurrent.ExecutionContext.Implicits.global
import scalax.io._
import scalax.io.JavaConverters._
import java.io.File

object Step4 {
  def main(args: Array[String]) {
    val in = args(0)

    val stream = EventProcessor.eventStream
    build(stream)
    stimulus(in)
  }

  def build(stream: Process[Task, String]) {
    Future {
      val t = converterProcess(stream).to(io.printLines(System.out))
      t.run.run
    }  
  }  

  def converterProcess(source: Process[Task, String]): Process[Task, String] =
    source.map(toRecord).map(toYear)

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def toYear(record: Vector[String]): String =
    record.lift(1) getOrElse "Unknown"

  def stimulus(in: String) {
    val queue = EventProcessor.q
    val input = new File(in).asInput
    input.lines() foreach { line =>
      queue.enqueueOne(line).run
      Thread.sleep(1000)
    }
  }
}

ストリーム処理の例としては以下の記事が参考になると思います。

ステップ5: ストリーム&フロー制御

ステップ5はステップ4で作成したストリームに、自前のフロー制御を組み込むという課題です。

この課題はちょっと難しいので、時間が余った人向けを想定しています。

package handson.reactive

import scalaz._, Scalaz._  
import scalaz.concurrent.Task  
import scalaz.stream._
import scala.concurrent.Future  
import scala.concurrent.ExecutionContext.Implicits.global
import scalax.io._
import scalax.io.JavaConverters._
import java.io.File

object Step5 {
  def main(args: Array[String]) {
    val in = args(0)

    val stream = EventProcessor.eventStream
    build(stream)
    stimulus(in)
  }

  def build(stream: Process[Task, String]) {
    Future {
      val t = converterProcess(stream).to(io.printLines(System.out))
      t.run.run
    }  
  }  

  def converterProcess(source: Process[Task, String]): Process[Task, String] =
    source.map(toRecord).pipe(toYear)

  def toRecord(s: String): Vector[String] = s.split(",").toVector

  def toYear: Process1[Vector[String], String] = ???

  def stimulus(in: String) {
    val queue = EventProcessor.q
    val input = new File(in).asInput
    input.lines() foreach { line =>
      queue.enqueueOne(line).run
      Thread.sleep(1000)
    }
  }
}

ステップ4のようにscalaz-streamのパイプラインにmapコンビネータで関数を合成する場合は、通常の関数を使うことができますが、フロー制御を組み込むことはできません。

フロー制御を組み込むためにはProcessモナドを引数にして、Processモナドを返す関数を作成し、pipeコンビネータでパイプラインに組込みます。

Processモナドを直接操作するので少し難しいプログラミングになりますが、フロー制御を自分で操作できるので色々な用途に適用することができるようになります。

ストリーム処理&フロー制御の例としては前項と同様に以下の記事が参考になると思います。

この課題は時間切れでボクも実装例を作ることができませんでした。

まとめ

OFPにおけるReactive Streamsの位置付けについて、FPを重視する方向性から整理してみました。

その上でプログラミングレベルでReactive Streamsを把握するための仕掛けとしてハンズオンの資料を紹介しました。

もちろんOFPのReactive StreamsはOFADにも、少なからず影響するはずです。この点はまだ考えが整理できていませんが、ブログで継続して検討していきたいと思います。

2016年10月28日金曜日

QCon Tokyo 2016

QCon Tokyo 2016で「オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える」と題してお話させて頂きました。


上記の個人用のSlideShareは文字化けが取りきれないので、きちんと読みたい方は会社のSlideShareの方を見ていただくとよいと思います。上記スライドもPDFをダウンロードしたものは文字化けしていません。

Reactive Streams

今回のテーマであるOFADについては別の記事で考えたいと思いますが、今回スライドを作っていて改めて以下のことを感じました。

  • Reactive Streamsは次のブレークスルーの起点になるかも

FP(Functional Programming)でI/Oを扱う技術としてIOモナドがありますが、その発展形として以下の2つの技術があります。

  • Operationalモナド(scalazではFreeモナド+α)
  • Processモナド(scalaz-streamの場合)

製品開発の中で、どちらの技術も使ってみましたがProcessモナドの方が圧倒的に楽なんですね。

Processモナドでは入出力などの作用に対する処理部は始端(source)と終端(sink)をパターンにしたがって実装すれば簡単に実現できます。source, sinkの抽象度が適切なので一度作った部品は色々な用途で再利用できます。また、(フロー制御用の)状態を管理する処理をProcessモナド内に組み込むためのメカニズムも持っていますが、これの実装もそれほど難しくありません。

一方、Operationalモナドは作用に対する処理はインタープリタとして実装して、自然変換のメカニズムでOperationalモナドの実行時に割り当てる必要があります。このメカニズムによりDI(Dependency Injection)の機能も実現できるので、その点では素晴らしいのですが、インタープリタという大きな仕掛けを作らないといけないので、単に入出力をしたいという目的には重たすぎると感じました。Scalaの場合はOOP側で自由に入出力できるので、このメカニズムを使ってまでFP化をすすめるニーズはなかなかないかも、という感触です。

このような感触が得られている中で、スライドページ「OOPとFPの協業」をまとめながら考えたのは「簡単に使えるReactive Streamsを使えば、多くの処理をFP化できる」ということです。

セッション内で説明しましたが、FPの方がOOPに対して、高品質(バグが出にくい)というメリットがあり、さらに持続的開発の中核作業であるリファクタリングで圧倒的な優位性を発揮する、というのがボクの主張点です。

このような観点からFPの範囲を大きく広げるReactive StreamsはOFP(Object-Functional Programming)にとって本質的に重要な技術なのではないかと感じました。

Reactive Streamsは大規模分散処理やストリーミング処理向けの専用機能という観点で取り上げられることが多いと思いますが、それだけではなく日常的なOFPにとって中軸となるプログラミング・モデルとなりうる点がより重要であると感じました。ここにさらに大規模、高頻度、ストリーミングがおまけでついてくるという切り口でのアプローチがよいと思います。

フィードバック

セッション後に2つほどフィードバックを頂きました。

OOPとFPの関係

スライドページ「OOPとFPの関係」ではFPでは以下のことが実現できないと説明しました。

  • 状態の更新
  • 動的束縛によるポリモーフィズム
  • 大規模開発(?)

この点について専門家の方から以下のような趣旨のフィードバックを頂きました。(文意はボクの理解によるものなので、正確な意図とはずれている可能性があります。)

  • 「動的束縛によるポリモーフィズム」と「大規模開発」は理論的に解決されており実用言語での実績もある。

「動的束縛によるポリモーフィズム」については、ボクの理解ではここがOOPとFPの違いだと思っていたので、理論的に解決されていて、さらに実用言語での実績もあるというという点は意外でした。

コンパイル時に継承関係が全て確定していれば、動的束縛部分を直和(+pattern matching)に落とし込むことはできるとは思いますが、共通ライブラリで定義したクラスの継承や、分割コンパイルといったニーズがOOP的には重要なので、この問題をどのように解決しているのか興味のあるところです。

大規模開発については、セッションではあまり深く触れていませんが、ボクのイメージでは以下のようなことがあってFP的には大変なのではと推測しています。

  • コンポーネント内に状態を持てないので、大規模システムの部品として利用するには大きな制約があるのではないか。
  • 「動的束縛によるポリモーフィズム」の問題でOOP的な継承が使えないとすると、API/SPIといったインタフェースを使ったコンポーネント部品化に制約がおきるのではないか。
  • OSGiなどを使った動的ローディングによるplugin機構は実現可能か。

機会があれば、このような観点から技術評価をしてみたいと思います。

どちらの問題も、Haslkellではできていないようですし、Scalaのロードマップにもないと思うので、Scalaで利用できるようになるのは当面なさそうとはいえそうです。

Reactive Streamsでできること

セッション後の質問時に以下のような指摘を頂きました。(文意はボクの理解によるものなので、正確な意図とはずれている可能性があります。)

  • 「HaskellでReactive Streamsを使って線形代数を行おうとしたがメモリが足りなくて動かなかった。セッションではReactive Streamsを使って大規模演算ができるとしているがいいかげんな主張ではないか?」

まず「HaskellでReactive Streams」についてはボクの経験外の話でもあり、Haskellライブラリの実装上の問題の可能性もあるのでここでは取り上げません。

線形代数に関しては、全データをメモリに展開して大規模な行列演算を行おうとすると、どのような技術を使ってもメモリ不足を起こすはずなので、この点でご質問の意図がよくわかりませんでした。

線形代数による大規模行列演算の応用にはReactive StreamsではなくSparkのMLLibのようなアプローチがよいのではないかと思います。

Reactive Streamsについては、ごくざっくりいうと「作用を始端と終端に切り離し、フロー制御ができるようになったイテレータ」なので、イテレータでできる範囲で大規模データ処理ができるということです。

具体的には、一定のウィンドウサイズの範囲でデータの一部をメモリに読込み、その範囲で処理を行ってメモリを開放する、という処理を繰り返す動きになります。

このような動きなので、原理的にはどのような大規模なサイズを扱っても大丈夫はなず、ということでセッション中は「1TBでも」とお話したのですが、この点に違和感を感じられたようです。

原理的には1TBでも大丈夫と思いますが、実証実験したわけではないので、この点は明らかにしておきます。

製品開発の中でReactive Streams(scalaz-stream)を大規模メール配信、大規模PUSH配信処理の実装で非常に便利に使っており、ほとんど問題も出ていないことから実用上は全く問題ないのでは、というのがボクの実感としてあり、その点を「1TB」として表現したしだいです。このサイズ表現問題が難しいのは10GB程度だと、現在のハードウェアではメモリに載せてしまうことも可能なので、わかりやすいインパクトのある例として使うのにはちょっと難しいことがあります。次の機会があれば、このあたりの表現を工夫したいと思います。

2015年4月6日月曜日

[OFAD] クラウド・サービスのモデリング

ここ数年Apparel Cloudの開発に携わっています。

Apparel Cloudはアパレル向けのクラウド・サービスを実現するためのサービス・プラットフォームで、サーバーサイドの実装はScala+ScalazによるMonadic Programmingを採用しています。

また、サービス企画からクラウド・サービス向けの仕様策定にはオブジェクト指向開発の伝統的なモデル体系をクラウド・サービス向けにチューニングしたものを用いています。

実システムの構築にこれらの新しい技術を適用して一通り材料も揃ってきたので、クラウド・サービス開発の枠組みとその要素技術について整理していこうと思います。

今回はモデリング体系の枠組みの整理です。この枠組みをベースに、個々の要素技術の詳細化を行っていく予定です。

枠組み

大枠では以下のような枠組みを考えています。

  • Service Platform as-a-Service
  • Object-Functional Programming
  • Object-Functional Analysis and Design

まず、クラウド・サービスの開発はスクラッチ開発ではなく、Service Platform上でのカスタマイズや追加機能をプラグインとして開発する形を想定しています。

また、プログラミング・モデルはオブジェクト指向と関数型を併用したObject-Functional Programmingです。並列、並行、分散、ストリーミングといったクラウド時代の要件を満たすためには関数型の導入が必須となるためです。

スクラッチ開発ではなくサービス・プラットフォーム上でのカスタマイズ+プラグイン、オブジェクト指向と関数型を併用したプログラミング・モデルという2つの大きな枠組みの変更は、当然ながら要件定義から分析・設計の一連の技術にも影響を与えます。

Service Platform as-a-Service

クラウド・サービスの開発では、スクラッチでシステムを組むというより、既存のサービスを組み合わせた上で、必要な部分だけ開発するという形が基本です。

このアプローチの核になるのがService Platformです。さらにService Platformをクラウドサービスとして提供したものをService Platform as-a-Service(SPaaS)と呼ぶことにします。

SPaaSが提供するプラットフォームを利用することで、クラウド・サービスの開発と運用をより簡単に実現することができます。

Apparel Cloudは、アパレル業界でのO2O用途向けのSPaaSということになります。

本稿を起点とする一連のブログ記事では、SPaaSの具体的な紹介や利用方法というよりも、SPaaSの存在を前提としたクラウド・サービス開発のモデリング(Object-Functional Analysis and Design)やプログラミング・モデル(Object-Functional Programming)を整理し、可能な範囲で体系化していきたいと考えています。

Object-Functional Programming

Object-Functional Programming(OFP)はObject-Oriented Programming(OOP)とFunctional Programming(FP)を融合させたプログラミング・モデルです。

FPはアルゴリズム記述の容易性や信頼性、保守性の向上がメリットですがOOPと比べると以下の問題点もあり、エンタープライズ分野では限定的な利用にとどまっていました。

  • 実行性能の低下
  • メモリ消費の増大
  • 難易度の高いプログラミング・モデル
  • (エンタープライズ的な意味で実績のある)安定した実行環境
  • 開発エコシステム(開発環境、クラスライブラリ、コミュニティなど)

しかし、クラウド時代に入って以下のような目的により積極的に採用する必要性が出てきています。

  • 並列・並行・分散プログラミング
  • 大規模データ処理
  • ストリーミング処理
  • 問合せ処理
  • DSL(Domain Specific Language)

特に、モナド(monad)という新しい概念をベースとしたMonadic Programming(MP)、MPをベースにしたFunctional Reactive Programming(FRP)が、本質的なプログラミング・モデルの変革をもたらします。

さらに、既存のOOPとの併用・融合も新しいテーマとなってきます。

本ブログでは今までも、これらのテーマについてScala+Scalazによるソリューションについて検討してきました。今後も引き続きこのテーマについて検討を進めていきますが、可能な範囲で今回提示した枠組みであるSPaaS、OFADとの関係についても考えていきたいと思います。

Object-Functional Analysis and Design

サービス企画の要求をまとめて、サービス開発に落とし込むためのメソッドはObject-Oriented Analysis and Design(OOAD)が基本になりますが、Service Platform as-a-Service(SPaaS)とObject-Functional Programming(OFP)という2つの中核的な技術変革により要件定義から分析・設計に至る一連のアクティビティにも大きなインパクトが出てくることが予想されます。

OOADについては、以前大学で教えていた時の内容を以下の2冊にまとめています。

現時点でもこの内容がボクとしての結論で、大きな枠組としては変わっていません。

ざっくりいうとボキャブラリとなるドメイン・モデルと物語であるユースケースを起点としたアプリケーション・モデルの二系統のモデルを軸に業務モデルからObject-Oriented Programming(OOP)による実装までの一連の開発アクティビティを一気通貫でカバーしています。

このOOADに対して、SPaaSとOFPの成分をどのように織り込んでいくのかという点が論点となります。本ブログでは、これらの要素を織り込んでOOADを拡張した方法論をObject-Functional Analysis and Design(OFAD)と呼ぶことにします。

まとめ

材料が揃ってきたので、クラウド・サービス開発の方法論の整備を始めることにしました。今回は議論のベースとなる枠組みを整理しました。

Monadic ProgrammingやFunctional Reactive Programmingを中心とするObject-Functional Programmingは、今まさに技術革新が進行中のホットな技術分野であり、本ブログの中心的なテーマとして取り上げてきました。今後も基本的には変わらないスタンスですが、今回提示した枠組みであるSPaaS、OFADとの関係についても併せて考えていきたいと思います。

Object-Functional Analysis and Designは、既存のOOADがベースなので既存のものと大きな違いはない想定ですが、SPaaS成分、OFP成分が入ることで相応の拡張が必要になりそうです。Apparel Cloudでの実践で得られた経験をもとに体系化を進めていきたいと思います。

2012年7月13日金曜日

クラウド温泉3.0@小樽

今年もクラウド温泉の季節がやってきました。

今回もかなり濃い内容になりそうで楽しみです。ustreamなしのオフレコなので、かなり踏み込んだ話を聞けるのもクラウド温泉の醍醐味ですね。

今回は2つほどスライドを用意する予定です。

  • Monadicプログラミング・マニアックス
  • OFP & OFADについて(仮)

連休明けぐらいから、この2つのスライドについて:

で行ったような感じで、準備がてらブログに内容をあげていきたいと思います。この時は、以下のようなまとめを作りましたが、今回も作ることになると思います。

Monadicプログラミング・マニアックス

「Monadicプログラミング・マニアックス」というタイトルにしていますが、内容的にはScalazを使った関数型プログラミング入門という感じになると思います。現時点でScalaとScalazを使っている事自体がマニア、ということで。

内容的には、去年のJJUGで使った「楽々Scalaプログラミング」を起点にMonadicプログラミングの技やプログラミング戦略を色々書いていこうと思います。

http://www.slideshare.net/asami224/scala-9728001

キーワード: モナド、モノイド、型クラス、代数的データ型、永続データ構造、並列プログラミング、SQL。

OFP & OFADについて(仮)

こちらは、二日目午後に小樽商大会議室で行うフリーディスカッション「OFP & OFADについて」のネタ投入用のスライドです。基本的には「Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標」をそのまま使う予定ですが、OFADについて少し書き足したいこともあるので内容を追加できればと思っています。

このスライド起点の議論はOFADよりのものを想定していますが、これとは別にOOPとFP、OFPを軸にした議論もあると思うのでそちらも楽しみです。OOPもJavaのように実用志向で手続き型寄りのものが主流になっていますが、元祖オブジェクト指向でメッセージングのメタファを持つSmalltalkやJavaScriptのようなプロトタイプベースなど色々なバリエーションがあります。

個人的には業務アプリのプログラミングパラダイムはOOPからOFPにパラダイムシフトしていくと思っているのですが、このタイミングでOOPやFPの原点に立ち戻って議論することで、新しいOFP像を垣間見ることができればと期待しています。

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年4月4日水曜日

Object-Functional Analysis and Designまとめのまとめ

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いました。その事前準備、まとめ、回顧の記事が一段落したのでリンクをまとめました。

今回のセッションはちょうど、クラウド・コンピューティングや関数型言語への理解が進んで、開発方法論やアプリケーション・アーキテクチャについて腰を落として考えるちょうどよいタイミングだったので、時間を取って色々な要素技術について改めて考えてみました。

今回50分のセッションで、それらの要素をすべて盛り込むことはできないので、事前資料や回顧の記事を書くことで、スライドを書きながら考えていたことを成果物としてまとめてみました。実際に、文章にまとめてみると自分的にも色々な発見もありますし、フィードバックから新たな情報も得られるので、非常に有益だったと思います。

まとめのまとめ

スライドとブログ記事を通じてひと通り考えてきたことをまとめると以下のようになります。

クラウド・アプリケーション・アーキテクチャ
CQRS/EDAが有力。EIP、DDDの技術も適用できる。
モデリング
引き続きOOモデリングが中心。CQRS/EDAをターゲットにイベントを中心としたモデリングが有効。
関数型モデリング
EDAのイベントコンシュマーでデータフローを記述する部分でOOモデリングを補完。OFPとDSLで実装とつなげる。
DSL
DSL指向フレームワークによるDSL指向プログラミングに移行。
OFP
DSL、並行プログラミングのニーズからクラウド・アプリケーションの主力言語になる。

個人的にはEDAの重要性を再認識したのが収穫で、EDAとOOAD、OFP、DSL、DDD、DCI、EIPといった技術との関係も整理することができました。

その上で、今後の大きな流れを考えてみると:

  • クラウド・アプリケーションはOFPとDSLで記述するようになる
  • EDAが新しい枠組みの上で再構築/再発明されていく

という事になろうかと思います。

スライド

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

まとめ

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

2012年3月30日金曜日

DCI (Data Context Interaction)

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

今回は「Data Context Interaction (DCI)」として用意した以下のスライドを説明します。

オブジェクト・モデリングの問題点の一つに、ユースケースからドメイン・オブジェクトへ真面目に責務の分散配備をしていくと、ドメイン・オブジェクトの実現が非常に重たくなってしまうという問題があります。

ドメイン・モデルそのものに由来するドメイン・ロジックをドメイン・オブジェクトで実現するのは、本来のオブジェクト指向の趣旨にも則っており問題ありません。

論点となるのはユースケース由来のアプリケーション・ロジック。このアプリケーション・ロジックを、(1)どのようにして適切な責務に分解して、(2)どのオブジェクトに分散配備していくのか、というのがオブジェクト・モデリングのホットスポットで、オブジェクト・モデリング手法を比較する上でのポイントとなるところです。

大きく、ドメイン・オブジェクトに配備していく派と、アプリケーション・ロジックを別オブジェクトで実現する派に分かれますが、理論的な主流派は前者、実際の開発者は後者を選ぶというねじれ状態になっています。

それはともかく、責務をドメイン・オブジェクトのみに配備していくと、システムを取り巻く文脈ごとのユースケースで必要な責務を集積した、巨大な責務の集りをドメイン・オブジェクトがまとめて実現することになり、(1)太ったドメイン・オブジェクトになってしまうという問題が出てきます。これに加えて、(2)色々なユースケースの断片が分散配備されることは、モジュール化、疎結合、高凝集という観点からも問題です。また、(3)モデルのライフサイクルの違いの扱いも論点の一つです。(3a)比較的安定しているデータモデル、(3b)比較的ライフサイクルの短いアプリケーション・ロジック、(3c)比較的高頻度に更新されるUIロジックを同じモジュールに配備すると、(3a)のUIや(3b)のアプリケーション・ロジックに引きずられ(3c)のデータモデル(を扱う処理部)にも影響が出てしまいます。

(1)は集中することの問題、(2)は分散することの問題、(3)は集中することの問題なので、それぞれ相反する事象であり、全てを一度に満足することは困難です。程よく集中して、程よく分散するバランス点を見出し、プログラミング言語での自然な実装手法を確立していく必要があります。

この問題に対する解として一時期AOPが注目されましたが、あまりうまくいっていないようです。柔軟すぎるメカニズムは、プログラムの安全性という意味で問題がありそうです。静的型付けの枠組み内でこの問題を解決するのが、安全性を担保するひとつの目安だと思います。

この問題を解決するアプリケーション・アーキテクチャとして注目されているのがDCI(Data Context Interaction)です。

このスライドでは、OFPが提供するトレイト、型クラスという新しい言語要素(「OFP新三種の神器」)が、DCIといった新しいアプリケーション・アーキテクチャを可能にするという点の説明を行う予定でした。

モデリングの流れ

DCIのモデリングの流れとしては:

  1. ユースケース・モデルの作成
  2. コラボレーションとコラボレーションから呼び出すロールを抽出
  3. コラボレーションを実現するアプリケーション・ロジックを分解してロールに配備
  4. ロールをドメイン・オブジェクトに編み込み。コンテキストで全体を統合。

となるかと思います。ボクが確認した範囲では、アプリケーション・ロジック専用のオブジェクトを生成したり、コンテキスト側でアプリケーション・ロジック(の一部)を実現するのではなく、あくまでドメイン・オブジェクトに編み込むロールにロジックを分割するようです。

つまり、この理解が正しければユースケースの責務をドメイン・オブジェクトに分割配備する伝統的なオブジェクト・モデリングの手法を軸としながら、ユースケース由来の責務をコンテキストに紐づいたロール側に配備することで、モジュール化を実現しているのがDCIということになります。

実装技術

ロールをオブジェクトに編み込む実装技術として、トレイトと型クラスが有力です。

現時点では、書籍の「Lean Architecture: for Agile Software Development」がDCIについて最も情報量があるのではないかと思います。この本では、DCIについての主な実装方法として、CとRubyの2つの言語での実装が主に取り上げられており、付録としてScalaも取り上げられています。

Cの実現はマクロを使ったものでやや強引な印象です。Rubyの実現はmoduleを使った動的型付ならではのメタプログラミング的な手法で、プログラマに負担をかけないのが美点です。

ただ、普通の静的型付け&OOPでの実装技術となるとなかなか難しく、Javaで実現する場合は相当量のボイラープレイト、繋ぎコードが必要になりそうです。この問題に対する解として期待されるのがScalaのトレイトです。

トレイトによる解

DCIでは、Scalaのトレイトによる実装を選択肢の一つとして挙げています。たとえば、「Lean Architecture」で紹介されているプログラム例の一部を以下に引用します。

val source = new SavingAccount with TransferMoneySource
val sink = new CheckingAccount with TransferMoneySink
source.increaseBalance(100000)
source.transferTo(200, sink)

SavingAccountクラスのオブジェクトを生成する時に、ロールを実現したTransferMoneySourceトレイトをwith句で編み込んでいるのがポイントです。この(ユースケース由来の)アプリケーションロジック向けの機能を事前にSavingAccountクラス側で提供しておく必要はなく、アプリケーションロジックでの必要に応じてロールを編み込みます。ユースケースから派生したロジックを、直接ドメイン・オブジェクトに分散配備するのではなく、ユースケースに紐づいたロール側にまとめておけるのでモジュール化も保てるというわけです。

型クラスによる解

Scalaでは、トレイとに加えて、型クラスという新しい言語機能が提供されています。(正確には暗黙パラメタと暗黙変換によってライブラリで型クラスを実現することができます。)型クラスは、基本的にはジェネリックプログラミングのカテゴリの言語要素だと思うのですが、Haskellで実用化され、関数型の枠組みの中でうまく機能しているのでたので広義の意味では関数型の特徴的な言語要素ということも可能かと思います。

Scalaでの実現であるScalazを実際に使ってみると"OOPにも型クラスの導入が可能で、実用上も有益である"のかなと思いますが、プログラミング言語理論は素人なので、このあたりの事情はよくわかりません。

ひとつ言えるのは、Scalaでは型クラスは非常にうまく機能しており、Scalaの提唱するObject-Functional Programmingの重要な基盤の一つになるだろう、ということです。

DCIをトレイトで実装する場合の問題点は、with句を使うコーディングとなるのでロールをドメイン・オブジェクトに編み込むところがハードコーディングとなることです。コンテキストが複数のロールを扱うときに、これらのロールを束ねるメカニズムもないので、このあたりで拡張性やモジュール化で問題が出てきそうです。

型クラスの場合は、コンテキスト毎にアプリケーション・ロジック、ロール、ドメイン・オブジェクトの対応関係の組を柔軟に指定することができるので、拡張性、モジュール化の能力に優れているのではないかと思います。そういう意味で、トレイトではなく型クラスが、ScalaにおけるDCI実現の本命ではないかと考えています。

DCIの評価

「このスライドでは、OFPが提供するトレイト、型クラスという新しい言語要素が、DCIといった新しいアプリケーション・アーキテクチャを可能にするという点の説明を行う予定」だったわけですが、改めてじっくり検討してみると、DCIはとても魅力的なアプリケーション・アーキテクチャではあるものの、実際のシステム開発への展開はまだまだ難しそうというのが実感です。

また、アプリケーション・ロジック専用のオブジェクトは第一級のモデル要素としては考えていないというのが、DCIがボクの好みとずれがある点です。ボクとしては、アプリケーション・ロジック、ロール、ドメイン・ロジックの3階層で責務の分割配備を考えたいと思っています。(このようにして抽出したアプリケーション・ロジックは、ドメイン・ロジックそのものなので、ドメイン・ロジック&ドメイン・オブジェクトとしてドメイン・モデル内に還元する、という考え方であればDCIとは矛盾しません。どちらの考え方にした方が、実際のアプリケーション開発で有効なのか、もう少し様子をみて判断したいと思っています。)

以上がOFADについてひと通り通して考えてみた上での実感です。

EDA

アプリケーション・ロジック、ロール、ドメイン・ロジックの3階層で責務の分割配備を行うためには、DCIでいうところのコンテキストを、アプリケーション・ロジックを配備するモデル要素に格上げするような、アーキテクチャが有効ではないかと考えます。仮にこのモデル要素をコラボレーションと呼ぶことにしましょう。(「コラボレーション」という用語はミスリードのような気もするので、他の名前を編み出したほうがよいかもしれません。)

このコラボレーションを、実際のアプリケーション・アーキテクチャに組み込む場所ですが、クラウド・アプリケーションでは、「EDAとオブジェクトと関数」で取り上げたEDAをベースとしたアーキテクチャが有力ではないかと考えています。

このアーキテクチャにあるコラボレーションをイベント・コンシュマーとしてEDA内に組み込むわけです。この時に、コラボレーションの通信相手となるロールを型クラスの機能を用いてドメイン・オブジェクトに編み込みます。

この方式では、イベント発生に付随した複数の疎結合のユースケースが並行動作することを自然に扱うことが事ができます。また、EDAの特質上、非同期処理も自然に扱えるのという長所もあります。

そういう意味で、DCIというアーキテクチャを単独で考えるのではなく、EDAのアーキテクチャの中にDCI的なアプローチも取り込みながら、EDAの枠組みで全体のアーキテクチャ(モデリング・ビュー、コンポーネント・ビュー)を考えていくのが、クラウド時代のOFADを整備していく上で有力なアプローチではないかというのが現時点での考えです。

諸元

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

このスライド作成の様子は以下の記事になります。

まとめは以下の記事です。

回顧は以下の記事になります。

2012年3月27日火曜日

オブジェクトの世界と関数の世界

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。
「オブジェクトの世界と関数の世界」として用意した以下のスライドを説明します。


このスライドは元々、以下の2つの情報を表現するために用意していたのですが、いろいろ書き込んでいくうちに、OFADでのモデル変換の流れの側面が大きくなってしまいました。
  • オブジェクトの世界と関数の世界は併存するのが必然
  • アプリケーション・モデルの実装側はできるだけ関数型にしていく
このため、同じような情報を持つスライドをセッションでは2つ省略しましたが、逆に、このスライドでは、「OFADでのモデル変換の流れ」の説明になってしまい、肝心なことの説明ができなくなってしまったので、この記事で補足します。

オブジェクトの世界と関数の世界は併存するのが必然

オブジェクト指向技術は、元々はシミュレーション技術に根っこがあり、その最も重要な軸は、(経験則的な)人間の認知モデル、メンタルモデルをそのままモデルとして使用している点です。
このため、要求モデルを記述する際に、普通の人が把握できる範囲のモデルにおさまることが期待できます。さらにオブジェクト指向言語を使うことで、要求仕様から実装まで同じセマンティクス、インピーダンス・ミスマッチなしで一気通貫に記述できるというのがOOADの最大のアピールポイントです。(OOの重要なメリットである情報隠蔽による安全性やポリモーフィズムによる拡張性は、その次ぐらいに重要な項目といえます。)
たとえば、DDDのUbiquitous Languageで、用語集からドメイン・モデル、さらにOOPでの実装まで一気通貫に共通化できるのはこのような理論的な背景があるからですね。
一方、振舞いの記述、アルゴリズムの記述は、情報科学や数学のバックグラウンドを持つ関数型に一日の長があります。(関数型はモデルの種類の名称としては一般的ではないので、以下では数理モデルという用語を使うことにします。)
とはいえ、数理モデルは、以下の問題があるので汎用的な意味で要求モデルを記述するモデルに採用するのは困難です。
  • 問題を数理モデルで記述できるとは限らない
  • 数理モデルは情報科学や数学のスキルがないと作成できない
  • 数理モデルは情報科学や数学の素養がないと内容を理解できない
要求モデルとして数理モデルで記述できる範囲のモデル、顧客が数理モデルの素養がある、という条件がうまくハマった場合には、一気通貫に要求モデルから実装コードの生成まで持っていけるので、極めて強力です。
このため、大枠はオブジェクト・モデルで考え、サブシステムまたはモジュール単位で数理モデルで要求モデルを併用するようなアプローチが有効でしょう。
いずれにしても、数理モデル一本ということは現実的には不可能なので、オブジェクト・モデルは必須です。全体の大枠は清濁併せ呑むオブジェクト・モデル、ぴったりとハマったところは数理モデル、ハマらなかったところはオブジェクト・モデルで記述して併用・併存するのが現実解です。

アプリケーション・モデルの実装側はできるだけ関数型にしていく

要求モデルでは、大枠はオブジェクト・モデルが必須で、一部条件を満たしたケースで関数型/数理モデルを併用することになります。エンタープライズシステムの場合、現実的には当面関数型/数理モデルを使うことはほとんどないでしょう。つまり、事実上要求モデルはオブジェクト・モデルということになります。
しかし、オブジェクト・モデルを実装する過程の中で、関数型を使えるポイントが色々とあります。一つは関数型言語を使った関数型的なプログラミングですし、より抽象的なモデルとしてはデーターフローが有力です。
図中のオブジェクトの世界では、現実世界→ドメイン・モデルのラインとやりたい事の物語→ユースケース・モデル→協調→状態遷移→アクションのラインがありますが、前者のドメイン・モデル・ラインはオブジェクト・モデリングがよいでしょう。
一方、後者のユースケース・モデル・ラインは、オブジェクト指向的によい実装方法がないので、可能なかぎり関数型にしていくのが望ましいところです。たとえばデーターフローで記述できるところを見つけて、これをデータフローモデル→関数型またはDSLで記述していくというアプローチですね。この部分をいかに関数型の方向に倒していくのか、というのが今後のプログラミング技術やモデリング技術の方向性を見ていく上で重要な視点ではないかと思います。
この場合、関数型で記述したアルゴリズムが扱う事実の情報、いわゆるファクトモデルとして、オブジェクトのドメイン・モデルを使うのがよい組み合わせです。

省略したスライド

内容が重複するので省略したのは以下の2つのスライドです。

上側は「 オブジェクトと関数の連携(3)」として用意していた図です。オブジェクトと関数の接点として、当面はデータフローが現実解かな、という内容です。


下側は「Domain-Driven Design (DDD)」で説明したとおり、OOADの中でのDDDの位置付けを説明する内容です。

OOPは不要?

昨日たまたま某所で教えてもらったのですが、以下のような流れもあるみたいです。(注意:一年前の記事です)
情報科学やアルゴリズムの勉強にOOPは不要でFPのみでよい、というのは一理あります。
現状ではエンタープライズ向けにはOOPが支配的な言語パラダイムである(COBOLがあるので"新規開発では"という注釈つきかもしれません)、システム、サブシステム粒度のモデルの記述方式に何を用いるのか、といった点は置いておくとしても、情報システム構築の要求モデルにオブジェクト・モデルが必須である以上、長期的に見てもエンジニアのスキルとしては引き続きOOPは必要と思います。
とはいえ、OOPが圧倒的、絶対的という訳ではなく、条件付きで有効というように相対化していく流れを象徴する出来事であるのは確かです。

諸元

当日使用したスライドは以下のものです。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)
このスライド作成の様子は以下の記事になります。
回顧は以下の記事になります。

2012年3月26日月曜日

Domain-Driven Design (DDD)

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いましたが、説明を端折ったところを中心にスライドの回顧をしています。

「Domain-Driven Design (DDD)」として用意した以下のスライドを説明します。



セッションの全体構成は以下のようになっています。

  • 関数型プログラミング
  • Object Functional Programming (OFP)
  • Object Functional Analysis and Design (OFAD)
  • 応用

この中で4番目「応用」は、今OOADやクラウド・プラットフォームで話題となっている技術がOFADでどのような影響を受けそうなのかということを考えてみる趣旨のセクションです。

具体的に考えてみることで、OFADへのイメージがより明確になるかなと思ったのですが、残念ながら時間の関係で省略することになってしまいました。「応用」は33枚目からなのですが、50分だと30枚ぐらいが目安ですね。

上記スライドは、その「応用」の一つとしてDomain-Driven Design (DDD)を取り上げたものです。DDDはいうまでもなく、エリック・エヴァンス氏の以下の書籍によるドメイン層の設計技法です。

OOADの中での位置付け

まず、OOADの中でのDDDの位置付けについて確認しておきましょう。以下は、当日は他のスライドと内容がかぶるので非表示にしていたスライドです。


この図ではOOADを、大きくクラス図を中心としたドメイン・モデリングとユースケース/協調を中心としたアプリケーション・モデリングの2つに分けています。

さらに、ドメイン・モデリングは、ドメイン・モデルそのものを作成する業務モデリング&要求モデリングとドメイン・モデルの実装方法をモデル化するシステム・モデリング&設計の2つに分けることができますが、DDDがカバーするのは後者システム・モデリング&設計の所です。

セッションのテーマであるOFADでは、"関数型の所は主にユースケース、協調に関するモデリング技術と関係を持ってくるのでDDDは直接影響を受けないだろう"、というのがこのスライドのDDDに関する部分の趣旨です。

このスライドを使っていた場合は、以下のようなことを口頭で補足する感じです。

まず、ドメイン・モデルという観点では関数型はルール・モデルに影響を与える可能性があります。とはいえ、ルール・モデルは関数型をさらに発展させた論理型と対応させるべきモデルなので、関数型の段階の影響はそれほどないのではないかと思います。

それより、関数型言語で実現するDSLでルール・モデルを記述するようなアプローチはありそうなので、その点は継続して動向を見ていく必要はありそうです。

[参考]業務モデリングと要求モデリング

ちなみに前者の業務モデリング&要求モデリングは、以下のような本が役に立ちます。

(翻訳は分かる範囲で載せていますが、翻訳品質は未チェックです。今回調べてみて分かったのですが、翻訳の方は良い本でもすぐに絶版になってしまいますね。早く電子出版に移行して欲しいです。)

DDDによる設計と関数型言語

OFADによってDDDの位置付けが変わったり、DDDによってOFAD側に影響が出るようなことはなさそうですが、DDDの設計・実装技法と関数型言語の関係はまた別です。

最初のスライドに戻ると、DDDのパターンの中でいくつか関数型的な手法が見られるので、それをピックアップしています。

まず、重要なのは:

  • Declarative Design - Domain Specific Language
  • A Declarative Style of Design - Composite Specification
  • Declarative Style

という形で宣言的な設計スタイルを推奨していることです。宣言的にすることでDSLを適用できたり、仕様の合成もできるようになるわけですね。関数型と相性がよい設計スタイルです。手続き型的な手法はこういったことができずに、どうしても手作業が多くなってしまいます。

OFAD的に翻案すると「いかに、手続き型な部分を減らして、関数型的な部分(DSLを併用)を広げていくのか」、というのが今後のソフトウェア設計の軸になるのではないかと思います。

直接関数型を意識しているのは以下の項目。

  • Side-Effect-Free Functions
  • Closure of Functions

Context Mapは、複数のドメイン・モデルを併用するときの技法です。最近だとLensのような代数的/圏論的な手法も登場してきているので、こういった手法を適用できる可能性があります。

Value Objectは、Entityの内容を持ちまわったり、協調の中で発生する揮発性の情報を持ちまわるためのオブジェクトで、DTO的な用途のオブジェクトを、モデリング上の一級市民に格上げしたものです。定義的にはID(がモデル上の意味)を持たないオブジェクト、オブジェクトの同一性の比較がIDではなくて、格納されている値の比較で行われるオブジェクトということです。

DDDでは、Value Objectとしていますが、基本的な値を表現するオブジェクトと、XML的な構造を持ったオブジェクトは分けたほうがよいと思うので、ボクは前者をValue、後者をDocumentと使い分けています。

Value Objectは、不変オブジェクトであることが推奨されており、関数型の代数的データ型や永続データ構造(関数型言語の技術マップ)といった技法で実現するのがぴったりはまります。(とはいえ、性能問題やプログラミングの手間もあるのでDDDには「Special Cases: When to Allow Mutability」というノートも書いてあります。実際問題として、Value Objectを完全に不変オブジェクトにするとプログラミングの手間が大変ですので、プログラマの習性としては分かっていても可変オブジェクトにしてしまいがちです。そこで、不変オブジェクトとこれをビルドするためのBuilderを自動生成しようというのが拙作のSimpleModelerのアプローチです。)

DDDは、筋のよいOO設計、OOP実装技法の経験則という側面が大きいと思いますが、DDDが出版された2003年当時OOP/OOAD全盛の中でも、関数型(的なアプローチ)の存在感はなかなか大きい感じがします。

関数型言語が実用化された今となっては、できるだけDSL&関数型的な手法で実現する範囲を大きくしていくことが、新しい経験則になっていくのではないかと思います。

具体的には、以下に書いたようなことですね。

諸元

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

このスライド作成の様子は以下の記事になります。

回顧は以下の記事になります。

2012年3月21日水曜日

Object-Functional Analysis and Designまとめ

3月19日(月)に要求開発アライアンスのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』を行いました。要求開発アライアンスの皆さん、多数お集まりいただきどうもありがとうございました。

当日使用したスライドは以下にあげておきました。(PDF出力ツールの関係で、当日は非表示にしたスライドも表示されています)

このスライド作成の様子は以下の記事になります。

要求開発アライアンス向けのセッションなので、OOADとOFPの関係というテーマ設定を行って考えて来たわけですが、現段階の材料ではびしっと決まらない…ですね。

Scala DSLを用いてデータフローを記述し、HadoopやESB上で動作させる技術が登場してきており、この技術を軸にOOADと関数型を組み合わせていくというのが当面の現実解として有効と考えられるので、セッションではこれにフォーカスした内容にしました。

OOAD的には一番普通のアプローチである協調(collaboration)を状態遷移経由でデータフローに結び付ける方法を中心に考えましたが、できあがったものを見るとあまり便利には見えないので、もう一段ブレークスルーが必要と思います。協調をイベント+データフローの集り+αで記述する方がエンタープライズ向けにはフィットしそうです。

プロセス計算

協調を記述する形式的な計算モデルの候補としてはプロセス計算が最右翼です。いわゆる形式手法のアプローチということになりますが、最終的にはこの方向とは思うもののエンタープライズ向けには現時点では研究段階というのがボクの認識なので、セッションではその点を軽く触れるにとどめました。

SOAの中核技術であるBPMN(のサブセット)が形式的だったと記憶しているのですが、Webを検索した感触だと、BPMNの説明で形式手法を全面に出している感じではなく、事実上はそういう使われ方はされていないのかなという印象です。

たとえば、InfoQでBPMを検索するとトップ記事が2010年の以下のものになります。

(サブセットが)形式手法であるというような記述も特に見られません。形式手法という観点では他の記事も大体似たような感じです。

BPMN&formalで検索すると以下のような記事(2008年)がヒットします。だいたい少し前の研究論文が頭に並んでくるのでSOA時代に一定の研究が進められていたのかな、という印象です。

データフローにフォーカスしたかったので、このあたりは省略したのですが、懇親会での材料投入にもなったと思われるので、入れておいたほうがよかったのかも、という気もします。

今考えてみると、BPMをSOAスケールで使うのではなく、局所的なデータフローの実現技術として使う方法もあるかもしれません。

関数型プログラミング

関数型プログラミングの最大の効用は、開発効率が高いことと思いますが、これだけだと実装技術として優れているということなので、OOAD的な開発手法との接点が出てきません。OOADで設計図を書いて、実装でScalaを使えば、という話になります。

そこで、セッションでは関数型プログラミングがシステム分析、設計に本質的に与える影響とは、という観点でまとめています。関数型プログラミングの最大の効用を省略しているので、念のためここに記しておきます。

そういう意味での、最大の効用は計算機科学、数学の成果物が、ちょっとした工夫でプログラミングの部品として活用できる道筋ができたことだと思います。直近では、並行プログラミングでそういった成果が活用できるようになるでしょう。

とはいえ、これは長期的スパンで漢方薬のようにじわじわ効いてくる効用なので、直近の実利としては説明しづらいのが難点です。

DSL

関数型言語の用途の一つにDSLがあります。関数型言語の自己拡張性というか言語を自然に拡張していける能力が内部DSLの実現に有効です。また、パーサコンビネーターといったメカニズムで外部DSLの実現も容易です。さらに、Scalaでは、さまざまな文法糖衣メカニズムによって、内部DSLの実現がより用意になっています。(こういった言語機能のScalabilityがScalaという名前の意図ですね。)

セッションでは、Spark、Scalding、Apache Camel Scala DSLの3つのデータフローDSLを紹介しました。それぞれHadoopやESBを利用するためのDSLとなっています。

モデリングとの接点で関数型言語によって実現されている実用技術を探すとこのあたりがアンテナに引っかかってきたわけです。

ここでは、モデリングとの接点なのでデータフローになりましたが、丸山先生の「エンタープライズ・クラウドの現在」で取り上げられていたPlay 2.0を始めScala DSLにはこの他にも様々な応用例が出てきています。

アプリケーションを構成する要素技術、ドメインごとにモデルをDSLで記述し、DSLの成果物を組合わせてアプリケーションを構築する...。以下の図はRelaxerの時代から使っているものですが、結局はこれかなという結論です。

この図を書いた当時はコンポーネントを生成して、これをスクリプトで結び付けるというアプローチを考えていたのですが、コンポーネント結合のDSLを使う手法が有力だと分かってきたことと、DI、AOPに加えてトレイト、型クラスといった新しい言語機能が加わってきたこともあって、実用化のハードルが低くなってきたと思います。

OFADより、DSL駆動というアプローチのほうが、当面の技術革新の方向にあっていると改めて感じました。

今後の方向性

懇親会で、萩原さんに最新の(クラウドスケールの)データベースの技術動向のお話を伺って非常に参考になりました。

一日寝かせて今朝思いついたのが以下のようなアイデア。(twitterより採録)

  • クラウドスケールの次世代DBの技術革新が、アプリ側に影響を与える一つはキー設計のところかな。GAEやAzureで議論になったキー設計はクラウド的に本質的。データフローを考えると、タプルの任意の冪集合をその段のキーとして使うようになるので、そのあたりも含めて。
  • SQLだと演算子が事前に決まっているけど、データフローは演算子を任意に追加できる。データフローを流れるデータと演算子の組について結合、可換をアプリレベルでどう記述していくのか、データベースやデータフローエンジンにどう情報を渡していくのかも論点。
  • ドメインモデル作成時に必要なこと(1)キー設計。ユースケースでドメインモデルのアクセスパターンを絞り込み、データ操作の局所性を抽出。同じ演算が適用されるデータが同じノード上に配置されやすいキーまたは複合インデックスを設計に織り込んでいく
  • ドメインモデル作成時に必要なこと(2)演算子設計。ドメインモデルでエンティティに加えて、データーフローに必要な代数的データ型(←エンティティをマップした物+中間データ)と演算子を定義。このために、ユースケースでデーターフローと演算子の抽出を行う。
  • エンティティに対する代数的データ型+演算子の組は、エンティティ操作に基本的に必要なものはドメイン・モデル、応用に特化したものはアプリケーション・モデル側で定義しておいて、後者はDCI的なメカニズムで後付けて編み込めるようにしておく。実装時には型クラスを使う感じ。

モデリングという方向では、ドメイン・モデル、ユースケース・モデル段階でこういった切り口でのモデリングを行うことが有効そうです。このあたりを今後の課題として考えていきたいと思っています。

2012年1月11日水曜日

SmartDoxの実装技術

昨日はSmartDoxの紹介をしましたが、 今日はSmartDoxの実装技術について説明します。
既存のプログラムの改良では、今まで積み上げてきたアーキテクチャや 使っているフレームワークの関係があって、 思い切ったコーディング方針の変更はなかなかできません。
SmartDoxは新規開発なので、練習も兼ねて ボクが認識している今風のScalaプログラミングを取り入れてみました。
  • Scala
  • sbt
  • conscript
  • GitHub
  • 関数型プログラミング
  • 代数データ構造
  • Scalaz
  • パーサーコンビネータ

Scala, sbt

実装言語Scala、ビルドツールsbtは現在の既定路線です。
sbtは、後発だけあって何かと使いやすいのと、Scalaプログラミング特有の 色々な事情をハンドリングしてくれるので、Scalaプログラミングでは必須です。
たとえば、Scalaは、コンパイルしたScalaバージョンの異なるライブラリのバイナリ互換が鬼門で、 JavaだとJARファイルが libname-1.0.jarとなるところを、Scalaだとlibname2.9.1-0.1.jarといった具合に 使用するScalaライブラリのバージョン(2.9.1)をファイル名に埋め込むのが、基本的な運用に なっていますが、sbtはこのあたりをうまくハンドリングしてくれます。
編集、デバッグにはEclipseを使っていますが、sbtのEclipseプラグインで「.classpath」を生成して、 ライブラリの依存性を取り込む運用にしています。
ただし、sbtだけでは力不足のところもあります。 以下の処理はsbtでやり方を見つけられなかったのでmavenとantを併用しています。
  • JavaのみのプロジェクトのJar作成(2.9.1を付けないJar名)
  • FTPによるmavenリポジトリへのアップロード(sftpならできるのですが、昔ながらのftpはダメみたい)

conscript

SmartDoxのインストールには conscript を使いました。
conscriptはGitHubに格納したアプリケーションの構成情報を使用して、 アプリケーションの依存関係を解決した上でローカル環境にインストールしてくれる インストーラです。
プログラムの提供側、利用者側のどちらもかなり便利なので、これから広く使われるように なるのではないかと思います。

GitHub

ソースコードの管理はGitHubを使っています。 GitHubはUIが使いやすいので、 このところ新規プログラムはGitHub上で開発していますが、 SmartDoxの場合は、conscriptを使うというためという理由もあります。
GitHubは、ソースコードのバージョン管理という枠組みを超えて、 conscriptのような形でソフトウェアリポジトリ的な使われ方もしてきています。 こういう新しい応用が出てくるので、使っていて刺激的ですね。

関数型プログラミング

関数型プログラミングは、ボクがLispをかじっていた頃(25年ぐらい前)はラムダ計算をベースに、 List、クロージャといった部品を使ってプログラミングしていくものでしたが (さらにいうと綺麗につくると性能がでないので、手続き型的なプログラミングにしたり、 nreverseといった破壊的な関数を使うのがバッドノウハウ)、現在は状況が一変しています。
まず ハードウェア性能の向上で、関数型言語の宿命だった動作性能は事実上あまり問題にならなくなっています。 Javaを使って大丈夫な用途であればScalaでも基本的には大丈夫と考えてよいでしょう。 RubyやPythonで大丈夫な用途であれば、Scalaだと逆に高速に動作しそうです。
技術的には、 モナド、型クラスという新しい技術が登場し、これが新しい関数型プログラミングの基盤になって います。
ボクも2008年にScalaを始める前は、 昔の関数型プログラミングのイメージで関数型言語を捉えていたのですが (List処理が便利で開発効率がアップとか)、 現在では全く別物と考えています。 モナド、型クラスはそれだけのインパクトのある技術ということが分かりました。 いつの間にか、こんなことになっていたのか、という感じです。
さらに、Scalaでは関数型プログラミング(FP)とオブジェクト指向プログラミング(OOP)の融合した Object-Functional Programming(OFP)という概念が提唱しています。
歴史的な蓄積やOOADからの連続性を考えると、 OOPは今後も軸の一つで在り続けることになると予想されます。 ここにFPの記述力をどのように融合させていくのかということが、 実務の世界では非常に重要になるのは明らかで、 OFPはこれは今後大発展する分野と考えています。
このあたりを意識しつつ SmartDoxの開発では、今風のFP的な技術をできるだけ使うことにしました。

代数的データ型

まずデータ構造として、代数的データ型(Algebraic data type)を使用します。 Scalaでは、代数的データ型は直接サポートされていませんが、 case classがこの目的での利用を想定して提供されています。 たとえば A Scala Tutorial for Java programmers にも以下の記述があります。 このあたりの背景は、 Object-Oriented Pattern Matching に詳しいようです。
In Java, such a tree would be represented using an abstract super-class for the trees,
and one concrete sub-class per node or leaf. In a functional programming language,
one would use an algebraic data-type for the same purpose. Scala provides the concept of case classes which is somewhat in between the two
今までは、内部データ構造はオブジェクト指向的な作り方にしていたわけですが、 SmartDoxではcase classを使ってimmutableな構造をにしてみました。
オブジェクト指向的な作りの場合、可変オブジェクトを複数のコンポーネントで共有して 少しづつ変更を加えていくプログラミングモデルになりますが、 代数的データ型(case class&immutable)にすると、コンポーネントで全複写しながら変換する プログラミングモデルになります。 immutableにすると、なにか不測の事態があったときの回避方法が限られるためOOP派的には 非常に怖い選択ですが、 FPでは避けて通れない道です。
SmartDoxでは、このプログラミングモデルにチャレンジしてみたわけです。 今の所、特に問題もなく、このプログラミングモデルでもやっていけそうな感触を得ています。
また、代数的データ構造を取っても、いざという時には型クラスの技法を使って 色々細工ができそうという読みもありました。 なにか問題が出てきたら試してみようと思っています。

Scalaz

Scalaz は、型クラスと純粋関数型データ構造を提供するScalaライブラリです。 Scalaの暗黙型変換、暗黙パラメータの機能を利用してHaskell的な型クラスを提供しています。
Scalaプログラミングを足掛け4年程やってきて分かってきたのは、 FPは関数の合成でプログラミングしていくのがコツということです。 Scalaでも提供されているMonadという仕組みは、この関数合成のメカニズムの一種で 普通の関数合成(composeやandThen, orElseなど)よりもより強力な関数合成を可能に します。 このMonadを活用したプログラミングスタイルをMonadicプログラミングと呼ぶようです。
Scalaの基本機能にもMonadはあるので、ScalaだけでもMonadicプログラミングは できるのですが、Scalazの強力な型クラス群を使用すると、さらに強力な Monadicプログラミングを行うことができるようになります。
Scalazが用意している型クラスはたとえば、Monoid, Functor, Applicative Functor, Monad といったもので、MonadをKleisli圏で合成するようなこともできるようになっています。 このあたりの型クラスを手足のように使えるようになるのが現在目標としていることで、 そのためにSmartDoxではできるだけScalazを使ってプログラミングする方針にしています。
Scalazを使うもう一つの効用は、Scalaで型クラスを使うためのよいお手本になるということ。 Scalaでは、型クラスの機能は直接サポートされておらず、暗黙パラメタ、暗黙変換の機能を駆使 して実装する必要があります。この実装技術をScalazで学ぶことができます。
型クラスは、フレームワークと実装クラスを疎結合に保ちつつ(静的な)多態性を可能にする 非常に強力な言語機能と理解しています。 Scalazの技法を使えば、Scalaでも型クラスを使った プログラミングが可能になるわけで、いずれ自分のプログラミング技法に 取り入れたいと考えています。

パーサーコンビネータ

SmartDoxでは、 SmartDox文書のパースにパーサーコンビネータを使ってみましたが、 驚くほど簡単にパーサーを書くことができることが分かりました。
パーサーの記述はBNF的なDSLになっていますが、基本的な動きとしては MonadicでかつApplicativeという感じです。 Parser[T]を返す関数がMonadic的な動き、その中で「^^」メソッドに渡すクロージャが Applicative的な動きと思います。
簡単なパーサーであれば、BNF的DSLという理解の範囲で使えますが、 細かいことをしようとするとMonadicかつApplicativeな動きを理解して いないと辛そうです。 パーサーコンビネータをある程度使いこなせるようになったのも、 Scalazを使ってMonadicプログラミングに慣れてきた効用かなと 思います。
パーサーコンビネータを使っていて分かったのは、 パーサーコンビネータは文字列以外にも使えるということ。 DOMなどのXML木やマイ代数的データ型からパターンマッチングで構造を抽出して ドメインモデルを構築するという用途には簡単に適用できそうです。 翻って考えてみればこのあたりがMonadicプログラミングの威力ですね。


以上、SmartDoxを素材にScala+Scalazを中心としたFP技術を概観してきました。 15年前はC→Javaによって、OOPがメインストリームのプログラミングパラダイムになり プログラミングの生産性が大きく向上しました。 それと同等の大きなムーブメントがFPの本格導入により起こりそうです。 もちろん前述したように、OOPは今後も軸の一つとなり続けるでしょうから、 OOPとFPを融合した新しいプログラミングパラダイムという形になるでしょう。
最後にどのプログラミング言語が残るのかは分かりませんが、現時点では Scala+Scalazで技を磨いておくのが有力な選択肢だと思います。