Janusパイプラインの例

檜山正幸 (HIYAMA Masayuki)
Tue Jan 25 2005:start
Fri Feb 04 2005:draft

Janusパイプラインの例をいくつか挙げる。テキストストリームの例、XML処 理の例、それとMVCの例が含まれる。図式(ダイヤグラム)の描き方とツール の話題にちょっと触れている。

目次

1. はじめに

この記事では、Janusパイプラインの例をいくつか挙げる。最初の例(2つあ る)はテキストスリームの例である。これらは、あまり面白くない例だが、基 本概念を確認するために役に立つ。次の例は、XML処理に関連している。XMLデー タのローディングと、DOMツリーの同期のためのパイプラインを挙げる。 最後の例では、 対話的アーキテクチャル・モデルであるMVC(Model-View-Controller)を Janusパイプラインに直列化してみる。 このMVCの例は、ひょっとすると面白いと感じていただけるかもしれない。もっ とも、ここで定義するMVCはオモチャのMVCだから、ただちに現実的な問題に適 用できるわけではない。MVCを取り上げる理由は、一見するとパイプライン的 方向性がないと思える有向グラフでも、パイプライン化できることを示し たいからだ。

ともかく、「習うより慣れろ」という諺もあるように、いろいろな例にあたっ てみると、だんだんJanusのコンポネントとプロファイルの感じがつかめてく るだろう。

以下では、 「Janus(ヤヌス)の紹介 -- stdin/stdoutからの入門」 を「紹介1」、 「Janus(ヤヌス)の紹介 2 -- フィルターからコンポ ネントへ」を「紹介2」として引用する。

2. テキストストリーム用のインターフェースとプロファイル

この節では、復習と準備を行う。まず、「紹介1」に 出てきた、テキストストリームを扱うための3つのインターフェースを再掲す る。

interface TextInput {
 boolean iseof();
 char getc() throws TextInputException;
}

interface TextOutput {
 void putc(char c) throws TextOutputException;
}

interface TextHandler {
  void handlec(char c) throws TextHandlerException;
  void closed(int code);
}

これら3つのインターフェースに基づくプロファイルが、 「紹介1」「紹介2」のな かで4つ出てきた。これらも再掲する。

/* 普通の(標準的な)フィルター */
profile StdFilter {
 input:
   use TextInput;
 output:
   use TextOutput;
}

/* 普通のフィルターどおしを繋ぐパイプ */
profile StdPipe {
 input:
   provide TextOutput;
 output:
   provide TextInput;
}

/* Push方式で入力するフィルター */
profile PushFilter {
 input:
   provide TextHandler;
 output:
   use TextOutput;
}

/* Push方式で入力も出力もするフィルター */
profile PushRelay {
 input:
  provide TextHandler;
 output:
  use TextHandler;
}

さらにいくつかのプロファイルを、ここで新たに導入しよう。次の2つのプロ ファイルを見ていただきたい。説明は下に続いている。(念のために注意して おくと、"input:"または"output:"が書いてないときは、入力ポートまたは出 力ポートがないことを示す。)

profile PushProducer {
 output:
  use TextHandler;
}

profile PushConsumer {
 input:
  provide TextHandler;
}

PushProducerは、入力ポートを持たず(ポートが0本)、出力ポートだけ を持つプロファイルである。非同期かつ自発的に文字を発生させるようなデバ イスをモデル化すると、そのプロファイルはPushProducerになる。典型的な PushProducerはキーボード(のソフトウェア的モデル)である。キーボードを 叩くのは人間だから、“打鍵=文字の発生”は非同期かつ自発的な現象(イベ ント)になる。

PushConsumerはPushProducerの逆で、入力した文字を消費してしまい、出力 はない。メモリ内に文字を溜め込むテキストバッファの例を後で出す。

さて、以下に挙げる2つのプロファイルは、ある種のパイプだが、Push方式と Pull方式を相互に変換する働きを持つ(*注1)

profile PushToPullPipe {
 input:
  provide TextHandler;
 output:
  provide TextInput;
}

profile PullToPushPipe {
 input:
  provide TextOutput;
 output:
  use TextHandler;
}
注1

PushToPullPipe型コンポネントは内部バッファを持つ。バッファが一杯/空 になると、プロセス/スレッドを止める必要がある。他の型のコンポネントで も、バッファリング、プロセス/スレッド制御を入れた方がいいときがある。

これらのプロファイルに対して、「紹介2」で説明 したような“極性付きポートを持った箱の絵”を描いてみるとよい。そう、 「習うより慣れろ」だからね。

以上の内容を、プロファイルのショートハンド(短縮)記法を使ってまとめ ておけば、以下のとおり。プラスがuseでマイナスがprovideだという約束を思 い出して欲しい。

  1. StdFilter = [+TextInput → +TextOutput]
  2. StdPipe = [-TextOutput → -TextInput]
  3. PushFilter = [-TextHandler → +TextOutput]
  4. PushRelay = [-TextHandler → +TextHandler]
  5. PushProducer = [→ +TextHandler]
  6. PushConsumer = [-TextHandler →]
  7. PushToPullPipe = [-TextHandler → -TextInput]
  8. PullToPushPipe = [-TextOutput → +TextHandler]

3. テキストスリームの例

2つの例を挙げる。

  1. 短いパイプラインの例: PushProducer → PushRelay → PushConsumer
  2. 長いパイプラインの例: PushProducer → PushToPullPipe →StdFilter → PullToPushPipe → PushConsumer

3.1. キーボード入力をメモリに溜める

keyboardコンポネントは、キーボードを単純な文字(キャラクタ)生成デバ イスとして抽象化したコンポネントだとする。 そのプロファイルは、PushProducerとなる。この事実を、keyboard : PushProducer と書こう。プロファイルPushProducerをショートハンド記法で 展開して、keyboard : [→ +TextHandler] と書いてもよいし、ブラケットを 省略して、keyboard : → +TextHandler でもよい。

tolowerコンポネントは、英大文字を小文字に変換する。そのプロファイル はPushRelayだする。だから、tolower : -TextHandler → +TextHandler と書 いてよい。コードは次のような感じだろう。

/* handlecの実装義務がある */
void handlec(char c) {
 outport.hadlec(tolower(c)); // handlecの利用権限がある
}

/* closedの実装義務がある */
void closed(int code) {
 outport.closed(code); // closedの利用権限がある
}

tolower(c)は、大文字小文字変換の関数だ。えっ?クラス/オブジェクトで 修飾されてない大域関数構文が出てくるのがけしからんって。別に僕はそんな こと気にしない、しらん。

characterStoreコンポネントは、受け取った文字をひたすらメモリバッファ に溜めるものとしよう。いつかバッファが一杯になって破綻するけど、メモリ がたくさんあればキーを叩くほうが飽きるから大丈夫(*注2)。この characterStoreのプロファイルはPushConsumerになる。つまり、 characterStore : -TextHandler → だ(矢印の右側には何も書かない)。

注2

飽きたら、Ctrl-DかCtrl-Zを打って知らせる。そうすると、closedがコール バックされる、っていうのも古ッ。

characterStoreのコードは、次のようなバッファが前もって準備されている なら簡単に書ける。

interface TextBuffer {
 /** バッファの末尾に文字を追加する */
 void add(char c) throws TextBufferException;
 /** バッファの内容をクリアする */
 void clear();
 // ... 以下省略
}

このTextBufferをラップすればいいだけだ。

/* handlecの実装義務がある */
void handlec(char c) {
 try {
  buffer.add(c)
 } catch(TextBufferException e){
  throw new TextHandlerException("buffer error");
 }
}

/* closedの実装義務がある */
void closed(int code) {
 // 必要ならなんかする
}

今まで出てきたコンポネントのプロファイルを並べてみると:

ほらね、出力側のインターフェース/極性と入力側のインターフェース/極 性がちゃんと同じ(極性は反対符号)になっているでしょ(って、当たり前だ、 そいう例を作ったんだから)。コンポネントの直列結合演算をセミコロン(「;」) で表すと、これら3つのコンポネントからなるパイプラインは次の表現を 持つ(*注3)

注3

セミコロンじゃなくて、単に空白で区切ったほうが見やすそうだ。 「keyboard tolower characterStore」となる。だが、直列結合にセミコロン を使うのは最近の傾向だから、一般的習慣に従うことにする。

パイプライン全体のプロファイルは[→]となる。これは、入力も出力も持た ないことを意味する。つまり、パイプライン全体としては閉じている(自己充 足的)。しかし、外部環境(人間)からの入力と内部記憶(メモリ)への蓄積 を行っているから、相互作用的(あるいはリアクティブ)に確かに動作してい るのだ。その動作とはすなわち、「キーボードからの入力文字を小文字にして バッファに溜め続ける」。

3.2. Push-Pull相互変換を含む例

前の例では、tolowerは、プロファイルがPushRelayであるコンポネントとし て登場した。今度の例では、tolowerが次のようなStdFilterとして実装されて いると仮定しよう。

while (!stdin.iseof()) {
 char c=stdin.getc();
 stdout.putc(tolower(c));
}

このtolowerを前の例と同様に使おうとすると、前後にPush-Pull相互変換の パイプを置かなくてはならない。そのパイプをそれぞれpush2pull, pull2push とすると、次のパイプラインが出来る。

4. XML処理の例

テキストストリームの例を理解すれば、XMLに関して似たような例はすぐに思 いつく。そこでこの節では、XML処理の例を出す。XMLプログラミングの知識が 多少あれば、容易に理解できるだろうから、説明は簡略にする。細部は各自補っ てください。コンポネント名は小文字で表すが、これはコマンド名のようなも のだと思ってください(*注4)

注4

つまり僕は、コンポネントをコマンドとして呼び出せるコマンドシェルがあ るといいな、とか夢想しているわけさ。

4.1. XMLデータのローディング

  1. JavaのReaderのような(あるいは古典的stdinのような)Pull方式キャラ クタ入力のインターフェースを持つreaderコンポネントを考える。
  2. saxReaderコンポネントは普通のSAXパーザーだとする。
  3. SAXイベントによりなんらかの変換処理(XSLT変換でもよい)を行うコン ポネントtransformerがあるとしよう。transformerは再びSAXイベントを 生成する。
  4. SAXイベントを受けて、対応するDOMツリーを構築するコンポネントを treeComposerとしよう。
  5. DOM実装として、domProviderがあるとする。

各コンポネントのプロファイルは次のようになる。

  1. reader : → -Reader
  2. saxReader : +Reader → +SAX
  3. transformer : -SAX → +SAX
  4. treeComposer : -SAX → +DOM
  5. domProvider : -DOM →

これらがパイプラインを作るのは明らかだよね。次の図のようになる。図で は、 saxReader -- SR, transformer -- T, treeComposer -- C, domProvider -- D と略記している。(readerコンポネントは描き忘れた:-))

FIG: XMLローディング処理

/* xml-load */

4.2. DOMツリーの同期

DOMツリーがあって、それに変更が起きるとミューテーション・イベントが適 当なポートから飛ぶとする。もっとも、DOMのミューテーション・イベントは 粗雑でダメダメかもしれないから、もっと適切な通知プロトコルが要るかもしれな い。ともかくここでは、なんらかのツリー変更通知を「ミューテーション・イベ ント」と呼ぶことにする。

DOM実装のなかで、Mutationポートにだけ注目すると、そのプロファイルは [→+Mutation]だと思ってよい。このMutationを聞いて(listenして)、別な DOMツリーを操作して同期させるようなコンポネントをtreeSynchronizerとす ると、そのプロファイルは[-Mutation → +DOM]となる。ここでインターフェー スDOMとは、DOMのツリー構築/ツリー操作APIだと解釈して欲しい(*注5)。 すると、実際にDOMツリー構築/ツリー操作APIを提供しているコンポネントの プロファイルは[-DOM →]となる。

注5

1本のポートで全DOM APIをサポートするのは賢いとは思えない。目的に応じ た何本かのポートに分けたほうがいいだろう。

3つのプロファイル[→+Mutation]、[-Mutation → +DOM]、[-DOM →]を見れ ば、これらを並べてパイプラインを作れて、ツリーどおしの同期リンクを構成 できるのはすぐわかるだろう。もう少し正確な議論を知りたければ、下のノー ト「残ったポートの取り扱い」を見てほしい。

さて下の図は、ツリーの同期を双方向化したものである。 2つのtreeSynchronizerを、向きを逆にして並べて(並列結合である)1つのコン ポネントとみなしている。つまり、次のような構成(component-composing operation)をしている。

この式で二項演算「+」は並列結合で、末尾に付けた「*」は入出力を取り替 える演算である。演算「*」は、記事「Janusについ て雑多なこと」の第5節で「双面神Janusの回れ右」と表現した操作である。 treeSynchronizer : -Mutation → +DOM だから、「回れ右」した(お尻に星 が付いた)treeSynchronizerのプロファイルは、 treeSynchronizer* : +DOM → -Mutationである。そして、 treeSynchronizer + treeSynchronizer* : -Mutation, +DOM → +DOM, -Mutation となる。図で確認されたい。なお、図で、TSは treeSynchronizerの略記、白抜き大矢印はパイプラインの方向である。

FIG: 双方向ツリー同期

/* 2way-tree-sync */

この例では、DOM実装の入出力取り替えも使っているから、もう少し図を眺め てみよう。この図で「DOM実装」とは、プロファイルが[→ +Mutation, -DOM] であるようなコンポネントだとみなしている。つまり、domImpl : → +Mutation, -DOM だ。もちろん、DOM実装のプロファイルがこうでないといけ ない理由はないが、とりあえずそう決めたのだ。入出力取り替えを行うと、 domImpl* : -DOM, +Mutation → となる。なお、ポートが複数あるときは、ポー ト順もひっくり返しておくと図と整合する。

図のパイプラインを、直列結合「;」、並列結合「+」、入出力取り替え「*」 を使って表現すれば次のとおり。

NOTE: 残ったポートの取り扱い

上の同期リンクの構成において、「Mutationポートにだけ注目する」とか、 「同期リンクを構成できるのはすぐわかるだろう」とかの言い回しは、実は粗 雑かつ素朴過ぎる。直感的には理解が難しくないと思うが、なかなかにやっか いな問題をはらんでいる。

ほんとにDOM実装がコンポネントとして存在したら、それはたくさんのポート を持つ大規模なコンポネントになるだろう。そのポート群の一部に対して他の コンポネントをアタッチするという行為のディノテーション(指示対象、理論 的な背景)はあまり明らかではない。

DOM実装の+MutationポートにtreeSynchronizerを接続したとき、DOM実装の他 のポートをどう扱うべきか? 記事「ETBダイアグラム」 第6節に出ている遮蔽器で隠してしまうという手もある。が、これでは不便 なときもあるだろう。残ったポートをストレートジャンクションで伸ばして、 後から来るコンポネントが使えるようにとっておく必要もあるだろう。

もっとも簡単な扱いは、「なにもしないでそのまままにしておく」だが、圏 論的計算ではそれが許されない。多圏(polycategory)の計算系を使えば、ポー トの一部だけを使用して、残りをそのままにできるので、いずれは多圏ベース の計算に移行したいと思っている。だが僕としては、多圏計算の解釈は通常の 圏のなかで行いたい。そのほうが心理的に安心できるからだ。

何も接続されずに残っているポートというのは、解放端またはサブシステム 境界ともいえるので、動的発展を考える上でも 重要なポイントになる。

5. MVC(Model-View-Controller)の例

MVCは有名なアーキテクチャル・モデルだから、ここで新たに解説はしないし、 その効能や功罪を詮索したりもしない。「そんなものがありますよね」という 前提で話をする。

下の図は、MVCの簡略な形式である。現実的な実装だと、MVCを変形したり、 けっこう複雑な構成にならざるを得ないときもあるが、ここでは理想的にシン プルなバージョンを採用する。

FIG: シンプルなMVC

/* mvc-triangle */

念のため、図の説明をしておく。テキストやグラフィックスのエディタをイ メージするとよい。

細い矢印付き線は、情報や制御の流れ(フロー)を示している。もっとたく さんの流れがあるが、面倒だから典型的な3本しか描いてない。

図に描いた矢印と反対方向への情報/制御フローも実際には存在する。Vから Mの編集APIを呼んだりするのは好ましくないのだが、そういうケースもままあ る :-)。

図の上部の白抜き大矢印は、「人間からデータへ」の方向を示しているが、 後でこの方向をパイプラインの方向として採用する。

さて、MVC三角形の直列化だが、実にたわいもない。Cの箱とMの箱を両手で持っ て、左右に思いっきり引っ張ってみよう。矢印は適当に伸び縮みするから、切 れる心配はいらない。ハイッ、両腕を拡げてヒッパッテー。

ほら、できました。

FIG: 直列化されたMVC

/* mvc-serialized */

CとMが左右に来たので、Vは真ん中の位置に落ち着く。CとMをつないでいる 「編集API」のワイヤーが、Vの下を素通りしていくが、伸びたワイヤーは自明 なジャンクションだと思いましょう(このジャンクションをIとする)。点線で描いて あるV'という箱は、VとIの並列結合である。つまり、V'=V + I だから、パイ プラインを表現する式は次のとおり。(この段落がよく分からないなら、 記事「ETBダイアグラム」にざっと目を通そう。)

このパイプラインを切り刻んで、左から右の順で素材を列挙すれば、下図のと おり。次の略記を使っている: 変更通知 -- Mu、編集API -- Ed、表示制御API -- Pr(「プレゼンテーション」 から)。

FIG: パイプラインの構成素

/* mvc-profiles */

6. 自由配線とブロックパッキング

最後のこの節は、まー、オマケです。

記事「ETBダイアグラム」第5節の補足「ワイヤー不 要論」で、ワイヤーを縦横無尽に引き回す配線法と、(本物のLEGOのように) デコボコをカチッカチッと接合してピッタリキッチリと配置するのと、どちら がいいかに触れている。

ありきたりの結論を言えば、一長一短である。ただし、まったく自由な配線 というのは、収拾がつかなくなるからやめたほうがいい。僕が推奨する原則は、 配線(ワイヤリング)を使ってもいいが、“ピッタリキッチリ配置と対応が付 く範囲”で配線せよ、というものだ。

では、「ピッタリキッチリ配置と対応が付く範囲の配線」が明確に定義でき るか? というと、… うれしいことに大丈夫なのである。例えば、MVCの三角 形を直列化する操作を考えると、これは図式書き換えである。前もっていくつ かのパラメータ(どの方向でパイプライン化するかなど)と書き換えの戦略が 必要だが、一定の手順で図式書き換えを行える。この手順は一種の正規化とも いえるし、最適化ともいえる。

いま僕は、図式書き換えアルゴリズムを完全に把握しているわけではないが、 世の中の定理証明系などに比べれば、ずっと簡単なアルゴリズムだろう。自由 配線図(とはいっても、制限された自由だが)のグラフィックスから適当なデー タ構造を作り出し、図式書き換えアルゴリズム(内部では項書き換えアルゴリ ズム)を対話的実時間で適用できるとすると、面白いアプリケーションが考え られる。

例えば、ウィンドウの右側(別に左でもいいけど)のペインでは、お絵描きツールやゲー ムのようなユーザーインターフェースで、自由配線図を描ける(くどいが、制 限された自由です)。すると、その配線図に対する“正規化”が即時計算され 左のペインに描かれる。正規化のほうもグラフィカルな表示で、ワイヤーなし のETBダイアグラムになる。つまり、左ペインには、ピッタリキッチリと配置 されたブロック群(レンガ壁みたいな感じだろう)が描かれる。

FIG: 右で自由配線、左にブロックパッキング

/* wire-block-ui */

右ペインでは、人間が感覚的に理解しやすいようにレイアウトの調整をして よいが、左のブロックパッキングは、記号的な表現(例えば、C;(V+I);M)と 完全な対応がある。つまり、右脳的感性と左脳的論理を同時に納得させるツー ルとなるのではないか。

もちろんこれだけでは、上のノート「残ったポートの取り扱い」でも指摘し たような、解放端や動的発展の問題には対応できない(静的な配置だから)。 だがそれでも、Chimaira的デザイン/コンフィグレーション・ツールの第一次 近似にはなりそうだ。