2019年7月29日月曜日

Kaleidox: XML

アクション言語を実用的に使用するにはXML, HTML, JSON, CSV, Excelといったクラウドアプリケーションで使用する各種データ形式を容易に扱えるようになっている必要があります。これらのデータをプログラム内に取り込んだり、データ形式間の相互運用が簡単にできることが重要です。

Kaleidoxでは各種データ形式の個々の操作性と、これらのデータ形式間の相互運用の両面について最適な記述が可能となるような言語機能を提供しています。

ここではまずXMLの操作についてみていきます。

XMLリテラル

KaleidoxはXMLを第一級の言語要素として考えていることもあり、リテラルで記述することができます。

以下のXMLを考えます。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

まずKaleidoxプログラムですが、以下のように上記XMLを直接XMLリテラルとして記述することができます。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

上記プログラムをsample.kに格納して実行すると以下のようになります。

$ kaleidox sample.k
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XMLリテラルで記述されたXML文書がそのまま評価され、評価結果がXML文書として出力されました。

REPL

REPLで同様の処理を行うと以下のようになります。

kaleidox> <account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...

プロンプトからXML文書を入力しています。

通常REPLのプロンプトからは改行を区切りとした一行の式を入力しますが、XMLやJSONなどの構造を持つリテラルはリテラル内に改行が入っていても入力することができます。

kaleidox> :show
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

ファイルから読み込む場合はURLを指定します。

kaleidox> file:sample.xml
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...

読み込み結果をshowコマンドで表示すると以下になります。

kaleidox> :show
Xml(String)
<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XPath

XML文書を処理する場合にはXPathによるXML文書内のデータアクセスが極めて有効です。

KaleidoxではXPathによるXML文書アクセスを行うことができます。

kaleidox> <account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>
<account>\n  <id>0001</id>\n  <name>taro</name>\n  <point>100</point>\n</acco...
kaleidox> /account/name
"taro"

XSLT

XML文書の操作ではXSLTによる変換も重要です。

変換対象のXML文書として前出のものを使用します。ファイルsample.xmlに格納しています。

<account>
  <id>0001</id>
  <name>taro</name>
  <point>100</point>
</account>

XML文書を閲覧用のHTML文書に変換するXSLです。ファイルsample.xslに格納します。

<?xml version="1.0" encoding="Shift_JIS"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="html"></xsl:output>

  <xsl:template match="/">
    <HTML>
    <BODY>
    <xsl:apply-templates/>
    </BODY>
    </HTML>
  </xsl:template> 

  <xsl:template match="account">
    <TABLE>
      <THEAD>
        <TR>
          <TH>Name</TH>
   <TH>Value</TH>
        </TR>
      </THEAD>
      <TBODY>
 <TR>
   <TD><xsl:value-of select="id"/></TD>
   <TD><xsl:value-of select="name"/></TD>
   <TD><xsl:value-of select="point"/></TD>
 </TR>
      </TBODY>
    </TABLE>
  </xsl:template> 
</xsl:stylesheet>

XSLT変換はxslt関数で行うことができます。

引数にはXSLリテラルまたはXSL文書URL、XMLリテラルまたはXML文書URLを指定します。

以下ではXSL文書URLとXML文書URLを指定しています。xslt関数実行の結果、XML文書を変換したHTML文書に得ることができました。

kaleidox> xslt file:sample.xsl file:sample.xml
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TB...
kaleidox> :show:print
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TBODY><TR><TD>0001</TD><TD>taro</TD><TD>100</TD></TR></TBODY></TABLE></BODY></HTML>

関数を省略してもXSLファイルが先頭の場合はxslt関数が実行されます。

kaleidox> file:sample.xsl file:sample.xml
<HTML><BODY><TABLE><THEAD><TR><TH>Name</TH><TH>Value</TH></TR></THEAD><TB...

Record

Kaleidoxはレコード/テーブル指向の言語なので、各種データをレコードに変換すると、より柔軟にデータ操作を行うことができます。

record-make関数でXMLからレコードを生成する事ができます。record-make関数はスキーマを与えなくてもXML文書の内容からレコードのスキーマを推測します。

kaleidox> record-make file:sample.xml
id:1,name:taro,point:100

Table

XMLをテーブルに変換することで、Kaleidoxが提供するテーブル操作機能を使ってデータ操作を行うことができます。

table-make関数でXMLからテーブルを生成することができます。table-make関数はスキーマを与えなくてもXML文書の内容からレコードのスキーマを推測します。

kaleidox> table-make file:sample.xml
Table[3x1]

生成されたテーブルデータを表示するためにshowコマンドを実行すると以下の情報が表示されます。

kaleidox> :show
Table[3x1]
┏━━━━┯━━━━┯━━━━━┓
┃id  │name│point┃
┣━━━━┿━━━━┿━━━━━┫
┃0001│taro│100  ┃
┗━━━━┷━━━━┷━━━━━┛
リスト

データ列を記述するXML文書として以下のものをsample-list.xmlとして用意します。

<accounts>
  <account>
    <id>0001</id>
    <name>taro</name>
    <point>100</point>
  </account>
  <account>
    <id>0002</id>
    <name>hanako</name>
    <point>200</point>
  </account>
</accounts>

ここからtable-make関数を使ってテーブルを生成します。

kaleidox> table-make file:sample-list.xml
Table[3x2]

生成した結果のテーブルは以下になります。

kaleidox> :show:print
┏━━━━┯━━━━━━┯━━━━━┓
┃id  │name  │point┃
┣━━━━┿━━━━━━┿━━━━━┫
┃0001│taro  │100  ┃
┠────┼──────┼─────┨
┃0002│hanako│200  ┃
┗━━━━┷━━━━━━┷━━━━━┛

まとめ

今回はXMLの操作について説明しました。

次回以降、HTML, JSON, CSV, Excelの順にデータ操作方法についてみていく予定です。

諸元

  • Kaleidox : 0.1.3

2019年6月2日日曜日

Kaleidox

前回SimpleModeling構想の中核のプロダクトとして開発中のアクション言語(Action Language)であるkaledidoxを紹介しました。

今回はこのkaleidoxのざっくりとした概要について説明します。

特徴

Kaleidoxは以下のプログラミング言語からヒントを得ています。

  • Lisp + Shell Script + Forth + COBOL

ベースとなるのはLispで、基本的にはLispインタープリターがプログラムを解釈実行する構造になっています。

Shell Scriptは、アクション言語として簡単なパイプライン構造でデータ操作を中心としたプログラミングが可能となるように、Shell Script的な文法をLispに対するシンタクスシュガーとして実現しています。

Forthは古のスタック指向言語ですがShell Script的文法と組み合わせてパイプライン・プログラミングを実現するために、Forth的なスタック操作の機能を提供しています。

COBOLは(1)レコード指向、(2)Divisionによる区画を参考にしています。

ソースコード

KaleidoxはGitHubで開発しています。

インストール

Kaleidoxは開発の初期であることもあり簡単にインストール可能なリリース版は用意していません。

興味のある方はソースコードからビルドしてみてください。

以下の手順でビルドすることができます。

$ sbt universal:packageBin

targetuniversalkaleidox-0.1.2.zip というファイルが作成されるのでこれを展開してインストールします。

sbtの機能を使って、debian, rpm, docker, graalvmのインストールパッケージを作成することも可能です。

Hello World

まずkaleidoxの簡単な使い方としてREPLによる実行をしてみます。

コンソールから引数なしでkaleidoxを起動するとREPLのプロンプトが表示されます。

$ kaleidox
kaleidox> 

kaleidoxはLispなので式を評価することでプログラムを実行します。

最も簡単な式はリテラルです。最も重要なリテラルである文字列を入力してみます。

kaleidox> "Hello World"
Hello World

評価の結果、文字列がそのまま返ってきます。

簡単な計算

次は関数の評価です。+関数で足し算を行ってみます。

kaleidox> (+ 1 2)
3

上記は普通のLispの関数評価ですが、kaleidoxの文法では以下のように括弧を外す表記ができるようになっています。

kaleidox> + 1 2
3

この例だけでは分かりづらいかもしれませんが、この文法によりShell Script的にプログラムを書くことができるようになっています。

まとめ

今回はかkaleidoxのざっくりした概要とインストール、Hello Worldまで紹介しました。

次回はJson, XML周りの扱いについて紹介する予定です。

諸元

  • Kaleidox : 0.1.2

2019年5月18日土曜日

SimpleModeling

Modegramming Styleブログではモデリングとプログラミングの一体化を目指してModegrammingというコンセプトを提唱しています。

このModegrammingを実現するための技術体系としてまとめようとしているのがSimpleModelingです。

SimpleModelingでは仕様定義と実装が一体化したモデル駆動開発の実現を目指して、方法論の整備とツールの開発を行っています。

SimpleModelingの基本的な方法論については以下の本にまとめています。

SimpleModelingの枠組みの中で、ここまでで以下のツールを開発してきました。

SimpleModelingによる方法論をモデル駆動開発につなげているツールとしてsimplemodelerを開発していましたが、この開発を通じて見えてきたのが自動生成したアプリケーションの動作基盤となるクラウドプラットフォームです。モデル駆動開発のための重要な要素技術としてクラウドアプリケーションプラットフォーム(以下CAP)が必要という認識にいたりました。

このCAPとしてを業務向けに開発を進めてきたものが Prefer Cloud です。Prefer CloudはPrefer Cloud Platform(以下PCP)というCAP上で動作しています。2012年から足掛け8年ほど開発を進め、一応の軌道に乗ってきました。

そこで、PCPの開発は進めつつ、次の活動としてモデル駆動開発を実現するためのSimpleModelingを再始動することにしました。

モデリング

オブジェクト指向開発方法論(OOAD)の一旦の完成時期を2004年だとすると、すでに15年が経過しており、その間にさまざまな技術革新がありました。

モデリングに関しては関数型言語の進化を取り込んだObject-Functional Analysis and Designの整備が必要だと考えています。

この問題に関しては以前から検討を続けており以下のような記事で検討を続けてきました。

また、クラウド環境を取り巻く以下のようなの新技術をカバーできるようにメタモデルの拡張やプロファイルの整備も行う必要があるでしょう。

  • AI
  • IoT
  • RPA
  • FinTech

プロダクト

SimpleModelingを支えるプロダクトとして以下のものを開発中です。いずれもOSSとして展開しています。

  • smartdox : 文書処理系 (前出)
  • simplemodeler : モデルコンパイラ (前出)
  • kaleidox :: アクション言語
  • arcadia :: ウェブ・フレームワーク
kaleidox

kaleidoxはオブジェクトモデリングとプログラミングをシームレスに連携させることを目的とするアクション言語です。

アクション言語はモデル駆動開発を成立させるミッシングリンクを埋める要の技術ではないか、というのが最近の技術的な興味で、これを具象化するためのアクション言語としてkaleidoxを開発してみました。

スクリプト言語としても面白いものに仕上がっていると思います。機能が多岐に渡るので次回以降少しずつ紹介していく予定です。

arcadia

モデル駆動開発を進める際にネックとなるのがWebフロントです。

サーバーサイドのサービスはRESTサーバーとして実現する場合、モデル駆動開発との相性がよい形で実現しやすいですが、Webフロントに関してはスクラッチの開発になるケースが多いと思います。

画面デザインの調整などどうしてもスクラッチ開発せざるを得ない部分もありますが、ビジネスルールの適用(パラメタの値域など)やアプリケーションロジックとのREST連携などモデル駆動開発の恩恵を得られると考えられるパーツも多いと思います。

モデル駆動開発では、後者の部分は自動生成しつつスクラッチ開発の画面部分との連携をとるためのフレームワークが必要となってくると考えています。

このようなモデル駆動開発向けWebフレームワークとして開発したものがarcadiaです。

kaleidoxの機能を一通りご紹介した後、arcadiaについても紹介していこうと思います。

まとめ

SimpleModelingの全体構想とプロダクトについてご紹介しました。

次回以降は当面SimpleModelingの中核プロダクトであるkaleidoxについて取り上げていく予定です。

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程度だと、現在のハードウェアではメモリに載せてしまうことも可能なので、わかりやすいインパクトのある例として使うのにはちょっと難しいことがあります。次の機会があれば、このあたりの表現を工夫したいと思います。

2016年10月11日火曜日

Object-Functional Analysis and Designふりかえり

クラウド時代のアプリケーション開発について、「クラウド・アプリケーション・モデリング」、「クラウド・アプリケーション開発のモデル体系」と考察してきました。

クラウド・アプリケーション開発では、実装時のプログラミングで「関数」が重要な構成要素となってきています。そうなると、この「関数」を上流のモデリングでどのように扱っていくのかということが重要な論点になります。

このような観点から、上流のモデリングから実装時のOFP(Object-Functional Programming)まで、オブジェクトと関数を融合させ一気通貫にまとめた開発方法論をModegramming StyleではObject-Functional Analysis and Design(OFAD)と呼んでいます。

Modegramming Styleでは2012年ぐらいからOFADについて考察を進めてきました。クラウド・アプリケーションのモデリングの検討を進めていく上で、OFADが一つの軸となると思います。このOFADについて、2012年に要求開発アライアンスでのセッション『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』向けにまとめたものがあります。

今回はこの2012年版OFADのふりかえりを行い、検討を進めていくうえでの論点整理を行いたいと思います。

Object-Functional Analysis and Design 2012

クラウド・アプリケーションの開発方法論を整備していくためには「関数」を理解した上で、関数とオブジェクトの関係を整理しないといけないという動機もあり、OFPの有力言語であるScalaを2008年から使い始めました。

ある程度「関数」とOFPについて勘所がつかめてきたところで、2012年に『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』というセッションのタイミングで一度まとめるタイミングがありました。

このセッションの内容は以下にまとまっています。

また関連して以下のような考察を行っています。

ふりかえり

今の目でOFAD 2012をチェックしてみましたが、それほど違和感はなく、以下の基本的な考え方については変更はありませんでした。

  • オブジェクトと関数の使い分け
  • オブジェクトと関数の連携
  • デザインパターン(代数的構造、圏論)
  • Domain-Driven Design (DDDD)

ただ、オブジェクトと関数の連携方法は2012年当時よりも手持ちの選択肢が増えたと思うので、その辺りは反映していきたいところです。

このタイミングで再検討したいのが以下の項目です。Reactive Streamsを始め、2012年以降、要素技術が大きく進化しているのでこれらの技術を取り込んだ上で新しい枠組みで考えてみたいと思います。

  • DSL
  • データフロー

以下のアーキテクチャ的な話題については2012年以降、特に大きな動きはなかったと思います。これらについてはOFADの再検討の中で対応を考えていきたいと思います。

  • EDA
  • DCI
  • CQRS

OFAD 2012以降の技術動向

OFAD 2012以降に起きた技術的な大きなムーブメントとしては以下の2つがあります。後者は我々の提案ですが、最近注目されているServerless Architectureに通じるところがあると思います。

  • Reactive Streams
  • Application Cloud Platform
Reactive Streams

より広い枠組みとしてはFunctional Reactive Programming(FRP)という切り口もありますが、Reactive Streamsの方が現状にあっていると思うのでReactive Streamsの用語を使います。

Modegramming Styleではscalaz-streamを中心にReactive Streamsについても考察を行ってきました。

また幾つかのセッションでお話させていただいたのでスライドとしてもまとめました。

純粋な数学的な計算はよいとして、システムの振る舞いをFunctional Programming(FP)でどのように記述するのかという点がFP実用化の重要な論点だと思いますが、モナドベースのReactive Streamsが一つの解としてブレークスルーの起点となりそうです。

そのような意味でOFADでの関数を考える上でReactive Streamsは重要な論点になります。

分析モデルの段階で、Reactive Streamsに対応するモデル要素を見つけることができればモデルから実装まで一気通貫でつなげるルートを確保することができます。

Application Cloud Platform

OFAD 2012の後、クラウド時代のアプリケーション開発ではクラウド・プラットフォームが重要な構成要素になると考え、その製品化を行う活動をしてきました。その成果として「Prefer Cloud Platform」をリリースすることができました。

Prefer Cloud PlatformのようなクラウドプラットフォームをModegramming StyleではApplication Cloud Platform(ACP)と呼んでいます。Prefer Cloud Platform自体もScalaによるOFPによって実装されていますが、この開発の中でOFPに関するノウハウ、OFADに対するヒントを蓄積することができました。

またACPによってアプリケーション開発の大きな部分を省略することができることが期待できます。こうなると、モデリングの目的はビジネスとアプリケーションの連携方法の分析とシステムの拡張方法の分析設計に絞られます。このような文脈の中での開発方法論ということもクラウド時代の開発方法論であるOFADに求められる点といえます。

まとめ

クラウド・アプリケーション・モデリング」を起点に進めている考察は『Object-Functional Analysis and Design: 次世代モデリングパラダイムへの道標』によって始動したOFAD 2012を最新技術動向やApplication Cloud Platform(ACP)の活用を前提に、2016年版OFADとして再構築を行うという目的のものです。

この検討を進めるためのベースとしてOFAD 2012を簡単にふりかえり、論点整理を行いました。

このフィードバックを活かして、次回はOFADのモデル体系について考えてみたいと思います。

お知らせ

10月24日に開催される「QCon Tokyo 2016」で以下のテーマでお話させていただくことになりました。

  • オブジェクト‐関数型プログラミングからオブジェクト‐関数型分析設計へ~クラウド時代のモデリングを考える

現在検討しているOFADについてのチュートリアル的な内容になる予定です。