Janusの紹介、第2弾である。古典的フィルター概念を拡張して、Janusコンポ ネントの概念を導入し、その概要を説明する。また、間違い/勘違いを避ける ための図式法を述べる。
Janus(ヤヌス)の紹介を続けることにする。ただし事前に、 「コンポネント・アーキテクチャ」と 「Janus(ヤヌス)の紹介 -- stdin/stdoutからの入門」 という記事に目を通していると仮定する。「Janus(ヤヌス)の紹介 -- stdin/stdoutからの入門」を「前回」として引用することがある(*注1)。
雑誌の連載みたいだね。実は、雑誌連載は僕にとってもっとも書きやすいス タイルなのだ。
さて今回の話題だが、Janusのややこしい点をうまく取り扱うための「絵の描 き方」を示す。その「ややこしい点」とは、コンポネントの“方向”や“順序” のことだ。Chimaira/Janusでは、矢印記号や矢印の形をした絵が頻出する (*注2)。いろいろな“方向”や“順序”があるので、間違いやすく、混乱しが ちだ。別に難しい話ではないのだが、言葉や記号だけでは表現しにくかった りミスを犯しやすいから、いつでも絵を描くことをお勧めする。絵(図式)は、 ほんとに強力な武器になる。
背景に、圏や高階圏(higher categories)があることが、矢印がやたら多く なるひとつの理由である。
この記事のなかで、Janusコンポネントの基本概念である極性と双方向性(対 称性)も導入する。極性/双方向性を視覚的に理解するためにも、図式は必須 だ。図式法をマスターすれば、Janusを使いこなすことができるだろう。
それで、今回の説明に使う素材は……、またパイプ&フィルターです :-) 他 の例も出したほうがいいかもしれないけど、それは別な記事に書こうと思う。 パイプ&フィルターは、(世間の認識はどうであれ)僕にとってはコンポネン ト・アーキテクチャのお手本だし、典型的なコンポネント・アーキテクチャだ とも思っている。まー、愛着があるから(*注3)、これを素材にすると説明 しやすいって事情もありますね。
パイプ&フィルターに関する思い出話を 「パイプ&フィルターの思い出」という記事に書いた。
前回の復習をかねて、 フィルターとパイプの概念を もう一度明確にしておく。フィルターとは、stdinから入力して、処理結果を stdoutに書き出すようなプログラムである。そしてパイプは、フィルターとフィ ルターを繋ぐ連結装置だ。フィルターfとフィルターgのパイプによる直列結合 は、伝統的に「f | g」と書かれる。ここで、パイプ記号(縦棒)は、単なる 形式的記号(構文的コネクタ)ではなくて、ソフトウェア的実体を指している と思うことにする(これ、重要なポイント)。だから、縦棒の代わりにpなど を使って、パイプラインを「f p g」のようにも書く。「f | g | h」なら「f p g p h」ってことになるが、 ここで2回登場しているpの実体は別物だとする。そうでないと、f、g、hを線形に繋 げない。
従来、パイプは「フィルターとは別なもの」「フィルターの結合をサポート するOS側の機能」のように考えてきたのだが、我々は普通のフィルターとパイ プを特に区別しないで、一様な枠組み内で捉える。普通のフィルター(狭義のフィ ルター)とパイプの違いはそのプロファイル(→ 前回 第5節)である。普通のフィルターとパイプは、それぞれ次のような プロファイルを持つのだった。
/* 普通の(標準的な)フィルター */ profile StdFilter { input: use TextInput; output: use TextOutput; } /* 普通のフィルターどおしを繋ぐパイプ */ profile StdPipe { input: provide TextOutput; output: provide TextInput; }
今後我々は、「フィルター」という言葉で次のようなソフトウェア的実体を 指すことにする。
たとえば、cat file において、catは標準入力を使ってない。入力リダイレ クト「<」を使わないなら、パイプラインの最初(左端)には、cat file のよ うに、stdoutへの出力を行うだけのプログラムが必要である。
単にメソッド諸元(名前、引数型、戻り値型、例外)を列挙しただけのイン ターフェースではなくて、制約条件(表明、契約、公理)も付いている“仕様” であることが望ましい。が、今はこの点に深入りしないことにする。
入力ポート/出力ポートそれぞれに割り当てるインターフェースと、そのイ ンターフェースを“利用する”のか“提供する”のかをハッキリと記述した宣 言をプロファイルと呼ぶのだった。上記5項目の定義のごとくに一般化され たフィルターの性格/特徴は、そのプロファイルで決定される。
なお、以下では、フィルター間で受け渡されるデータ(の最小単位)はキャ ラクタだと仮定しよう。別にそう仮定する必要はないのだが、あまり余計なこ とを考えたくないから、この前提で話をする。
説明に使う例の説明をしておく。以下では、Push型のパイプラインを考えて みる。まず、次のインターフェースTextHandlerを思い出そう(→ 前回 第7節)。
interface TextHandler { void handlec(char c) throws TextHandlerException; void closed(int code); }
このインターフェースは、XMLのSAX仕様のように、データを受け取る側が実 装すべきハンドラー(コールバック・プロシージャ)を規定している。データ を送る側は、相手のhandlecというメソッドを呼び出して通知する。
ここで、次のプロファイルを考えてみよう。
profile PushRelay { input: provide TextHandler; output: use TextHandler; }
PushRelayは、インターフェースとしてTextHandlerだけを使ってパイプライ ンを構成するためのプロファイルである。プロファイルPushRelayを持つフィ ルターは、データ入力用にハンドラーhandlecを実装し、データ出力には他の フィルターのハンドラーhandlecを呼び出す。fとgが共にPushRelayフィルター なら、fとgは直列結合可能である。PushRelayフィルターだけでパイプライン を構成するのはたいてい無理だ(両端はどうする?)が、パイプラインの中間 部分にPushRelayフィルターを何個も並べることができる。
何もしないでデータを流すPushRelayフィルターは次のように書ける(*注6)。 なお、変数outportは、出力ポート(あるいはその先につながっているフィル ター)を指すTextHandler型変数だとする(*注7)。
/* handlecの実装義務がある */ void handlec(char c) { outport.handlec(c); // outportのhandlecの利用権限を持つ } /* closedの実装義務がある */ void closed(int code) { outport.closed(code); // outportのclosedの利用権限を持つ }
「書ける」とはいっても、擬似コードで書いているだけだ。Javaなどのホン トのプログラミング言語で実装しようと思うと、メソッド呼び出しの同期/非 同期、スレッディング、ポートをどう実現すべきか、などで悩んでしまうだろ う。呼び出しの中継、ポートの実現、ポートを繋ぐような作業は、コンポネン トではなくて、コンポネントの動作環境や制御機構が担当するものだ。つまり、 “Janusコンテナ”や“Janusシェル”が必要になるのだ。
コンテナやシェルの実装についての僕の試案も、時期がくれば説明したい。 だが、やり方が一つってわけでは全然ないから、興味を持った方は各自で考え てみればよい。僕みたいな白頭爺が考えるより、よほど いい案が出るだろう。
このoutportって変数はちょっと正体不明だろう。プロファイル宣言における "output:"とはどう関係するのだろうか? 考え方は2つある。1つの方法は、ポー トとプログラム内で使う変数の関係(バインディング)を、毎回明示的に宣言 する。もう1つは、なんらかの約束事により、ポートとプログラム内で使う変 数の関係は自動的に決まるようにする。変数outportは、このいずれかの方法 で、出力ポートと関連づけられていると仮定している。
ところで、インターフェースTextHandlerを経由してデータを受け渡すとき、 “データの受け側=ハンドラーの呼ばれ側”は、データ受け取りのタイミング を自由に制御することができない。外部から一方的にキャラクタを押し込まれ る。たとえば、処理を一時的に休止したくてもどうにもならない。そこで、昔 なつかし“フロー制御”(*注8)のインターフェースFlowControlを導入しよう。
interface FlowControl { boolean isactive(); void stop() throws FlowControlException; void start() throws FlowControlException; }
コンソールへの出力がスクロールで流れていってしまうとき、Ctrl-Sを押し たりしませんか? しないかな、イマの人は。
isactiveによって、いまデータが流れている状態かどうかを知ることができ る。stopを呼べばデータの流れが止まり、startで再開する。まー、あたりま えの仕様だね。が、せっかく定義したインターフェースFlowControlを割り当 てるべきポートがない! だって、inputにもoutputにも割り当てられないでしょ、 この2つはデータ用のポートだから。さて困った。で、次節に続く -- いやっ、 次節ではちょっと別な概念を準備しよう。
前回において、最初のサンプルコードとしてCによる フィルターを使ってしまった。それに引きずられて、関数/メソッドの名前は、 iseof、handlec、isactiveなんてことになっている。あーあ。首尾一貫したネー ミングも良し悪しだわ。もう、isEndOfFile, handleCharacter, isActiveに変 えたい気分だわさ。
ここで、極性という概念を導入しておこう。極性については、 前回の第9節でほんのちょっと(いわくありげに) 触れたが、ここで正式に定義する。この極性というやつは、Janusのキーコン セプトの1つである。
‘極性’(polarity)とは、「電気のプラスとマイナス」、「磁石のNとS」、 「論理の真と偽」のような、二値の量のことである。また、ナニカに二値の量 が割り当てられているとき、そのナニカは極性を持つ、とか偏極している (polarized)というのだ。「偏極ナントカ」の代わりに、「符号付き (signed)ナントカ」とか「荷電(charged)ナントカ」というときもある。 場合によって、向き(orientation)が極性を与えることもある。
この記事では(たぶん、Chimaira/Janus全体で)、極性の値としてプラスと マイナスを使う。よって、極性値の集合は{+, -}である。P={+, -}だとして、 集合X上の偏極(符号割り当て、荷電)とは、写像p:X→Pのことである。 集合Xと偏極pの組(X, p)は、偏極(符号付き、荷電)集合と呼ぶことになる。
さて、P上の唯一の演算としてσ:P→Pを導入しよう。これは、 σ(+)=-、σ(-)=+ で定義される。ようするに、プラスとマイナスをひっくり 返す演算である。「唯一の演算」と言ったのは、論理値の集合(booleanだね) なら、ANDとかORとかも考えるけど、極性値の集合Pでは、そんなものは考えな いのだ。意味のある演算はひっくり返しσだけだ。
このひっくり返し写像σには、むやみと色々な呼び名がある(それだけ、 いろんな所で登場するってことだね)。反転とか逆転を意味する reverse, reversion, conversion はもちろん使う(*注9)。 別記事「ETBダイアグラム」第5節の注で触れたが、 交差や取り替えを意味する cross, crossing, flip, swap, twist, symmetry, permutation, transposition, exchangeなども使っていいだろう。幾何的な比 喩からは、鏡像、鏡映(reflection、mirroring)などの用語もある。代数で は、2回行うと元に戻るような演算を対合(involution)(*注10)とか共役 (conjugate, conjugation)と呼ぶので、このての言葉も使われる。そして、 論理の用語を借りて、否定(negation)と言う場合もある。で結局、我々はど れを使うかというと……、悩んじゃうのだけど、平凡に「反転」にしましょう かね。
inverse, inversionは、逆元や逆写像の意味があるから使わないほうがいい だろう。
Kelly & Laplazeがcompact closed categoryの構成に 使っているinvolutionはまったく別なものである。彼らは、極性(符号)が付 いた点を結ぶ“ヒモの束”をinvolutionと呼んでいる。
さて、極性を何の目的に使うかというと、メソッド呼び出しの“向き”を指 定するために使う。どういうことかというと、プロファイル宣言において、 useとprovideっていうキーワードを使ったけど、useをプラス、provideをマイ ナスという極性値(符号)で区別する。つまり、use TextInputの代わりに 「+TextInput」、provide TextHandlerの代わりに「-TextHandler」と書いて もいい。
単に表記法を変えただけで何がうれしいかって? まずとりあえず、図を描き やすくなる。図が描きやすいなら、考えやすくなるし、説明も楽になる。それ に、物理、電気工学(特に回路図)、論理(線形論理)などの知識・技法を拝 借できるようになる。
ところで、use、provideのどちらをプラス(もう一方がマイナス)にするか は、まったく恣意的(どっちでもいい)なのだ。必然的、絶対的な理由や根拠 なんてなくて、エイヤッと決めればいいことだ。だが、どっちでもいいことだ と、かえって悩む。僕はすごく悩んでしまった。今も悩んでいる。ある日気が 変わって、プラスとマイナスを逆にしたくなる、ってこともあるかもしれ ない。useをプラスにした理由は、下のノートに書いておく。
メソッド呼び出しを、“呼び側”から“呼ばれ側”に 向かう矢印で表す(こ れを後で「呼び出し弧」と呼ぶ)。コンポネント境界を越える呼び出しでは、 呼び出しの矢印の根本がuse側で矢先がprovide側になる。
さて、矢印の両端のどちらにプラスを割り当てるべきか。電流は「+ → -」 と流れるから、根本がプラスがよいか? だが、実際の電子の流れは「- → +」 だし、数学でも「- → +」が普通だ。点pから点qに向かう矢印(1次元有向単 体)に対して、その境界は q - p と表すのだけど、これは (+q) + (-p) で、 始点にマイナス、終点にプラスの極性(向き)を与えたものだ。
それじゃ、「- → +」がいいかというと、まだ考慮すべきことがある。イン ターフェースをuseするとは、そのインターフェースを能動的(active)に使 うことだ。一方、provideするインターフェース(の実装)は、受動的 (passive)に呼び出しを待つことになる。プラス・マイナスは、英語だと positiveとnegativeとも言うけど、語感としては、positiveとactive、 negativeとpassiveが対応した方が自然な気がする。この方針だと、「+ → -」 になる。
結局、日常的な感覚では、useがプラスでprovideがマイナスなのが自然な気 がしてきた。それで、「use=プラス=positive=active」、 「provide=マイナス=negative=passive」という対応を選んだ。
話を戻そう。2つのフィルター間でフロー制御を行いたいのだった。そのため のインターフェースFlowControlは定義したが、このインターフェースを割り 当てるべきポートがない。もうこれは、ポートを増やす以外に手がないでしょ、 やっぱり。つまり、データ入出力用のポート以外に、制御用ポートを付けるの だ(*注11)。
なんだか、RS-232Cのハードウェアフロー制御の話をしているような気分だぞ。
そうなるとだね、先に導入した「フィルターの定義」が早々と破綻してしま う。なぜなら、フィルターとは、入力用に1本、出力用に1本のポートを備えた ものとして定義したからだ。制御用ポートはポートとして勘定しないという逃 げ道はあるが、そうではなくて、フィルターの定義をさらに拡張して、任意本 数のポートを許すことにしよう。そのほうが建設的、発展的だからね。
もっとも、ポートが5本も10本も出たヤツをフィルターと呼ぶのは抵抗がある のも事実だ。そこでいっそ、フィルターの拡張概念を‘コンポネント’と呼ぼ うじゃないの。ここで、「そもそもコンポネントとは、カクカクシカジカ…」 とか言い出すのはやめようね -- それは建設的じゃない、発展的じゃない、つ まり不毛だから。
一般のコンポネントは、任意の本数の入力ポートと出力ポートを持ってよい とする。「任意の本数」には0本も入るから注意してね。と、そういうわけで、 たとえば、3本の入力ポートと2本の出力ポートを持つコンポネントは次のよう な図で表す。
/* box-with-poorts */
ん? 「ボックス」ていう書き込みはなんだ?とお思いだろう。ゴメン、これ は「コンポネント」と書くべきだ。実はモノグサして、 「ETBダイアグラム」という記事の最初の図をそのま ま流用した。この「ETBダイアグラム」は、図の描き方を集中的に説明した記 事だ。いまここで「ETBダイアグラム」を読みにいかなくてもいいけど、後で 「ETBダイアグラム」にもざっと目を通しておくといいだろう。
さて、Janusにおいては、ポートの本数が自由というだけでなく、それぞれの ポートに極性(プラスまたはマイナス)が付くという点がとても重要だ。 例として、フロー制御を持ったコンポネントの図を描いておこう。
/* flow-control-ports */
この図で、入力から出力への向きは左から右だ。コンポネントの各ポートに は、極性とインターフェースが割り当ててある。極性プラスはuseに対応し、 極性マイナスはprovideに対応するのだった(前節を参照)。2つのポートをつ なごうとするとき、プラスのポートはマイナスのポートと、マイナスのポート はプラスのポートとつなぐ -- 電気回路の結線を思い出してね! もちろん、 インターフェースは一致してなくてはならない。上の図のコンポネントAとコ ンポネントBは、“AからBへ”の順序でつなぐこともできるし、“BからAへ” という順でも直列結合できる。だが、一般にはそうウマクはいかないから注意 せよ。
任意本数のポートを許すと、プロファイル宣言の構文も拡張しておく必要が ある。とはいっても、その拡張方法は自明に近い。use宣言文とprovide宣言文 を複数書いてもよいだけのことだ。下に例を挙げよう。なお、プロファイル宣 言において、プロファイルの名前が不要なときは、無名のプロファイルも許す ことにしよう。
profile { input: provide TextHandler; output: use TextHandler; provide FlowControl; } profile { input: provide TextHandler; use FlowControl; output: provide TextHandler; }
ついでに、プロファイル宣言の略記も導入しておく。プロファイルの全体は、
[入力 → 出力]
の形とする。入力と出力の部分には、インターフェース名
の前に極性(プラスかマイナス)を付けたものをカンマで区切って並べる。上
の2つのプロファイルはそれぞれ次のように略記できる。
これまでの話で、フロー制御を持ったPush型パイプラインを設置する準備は 整った。例えば、次のようなコンポネントA, B, Cを考えてみよう。
/* sample-profiles */
コンポネントAは、Pull型のストリームからgetcを使って文字を自発的に吸い 出す(ポンプの役割だ)。そして、出力先のハンドラーを使って文字を押し込 む。また、コンポネントAはフロー制御をサポートしているから、stop/start によって、文字の出力(押し込み)の休止/再開を指令できる。
コンポネントBは、フロー制御付きPush型パイプラインの中継部分になってい る。コンポネントBのなかで、例えば大文字→小文字変換(tolower)を行って もよい。
最後のコンポネントCは出力ポートを持たない。つまり、データストリームの 最終消費者ということだ。わざとらしい話だが、Cは小さなバッファしか持た ず、画面に非常にゆっくりと文字を書き出すプログラムだとしよう。文字がド ンドンやって来ると困るから、Cはフロー制御を使うことになる。
A、B、Cをこの順でつないだパイプラインは次の図のようになる。
/* sample-pipeline */
この記事の冒頭で、「Janusの『ややこしい点』とは、コンポネントの“方向” や“順序”のことだ」と述べた。これはホントに間違いやすい。この点をこれから 説明する。前節の「コンポネント達」と「パイプラインの図」を見返しながら 読んで欲しい。
まず、1個のコンポネントにおいて、入力側と出力側がある。今まで常に、入 力側が左、出力側が右になるように図を描いたがいつでもそうとはかぎらない。 入力側が上、出力側が下の図もあるだろう。ともかくまずは、1個のコンポネ ント内の入出力をハッキリと識別する必要がある。
次に、「パイプラインの図」だが、この図にある白抜きの大きな矢印は、パ イプライン全体の方向を示している。一方、箱をつないでいる細い矢印はメソッ ド呼び出しの方向を示している。この細い矢印は‘呼び出し弧’(call arc) と呼ぶ。Janusの特徴は、双方向性を導入したことだから、呼び出し弧の方向 は「右から左」と「左から右」が混じる。くれぐれも、“パイプライン 全体の方向”と“メソッド呼び出しの方向”を混同しないように。極端な例で は、“パイプライン全体の方向”と“メソッド呼び出しの方向”がちょうど正 反対になることもある。
それでも、テキストストリームの例では、“パイプライン全体の方向”はキャ ラクタ・データが運ばれる方向に一致しているから、方向概念はハッキリとし た意味を持つ。だが、対等に対話する2つのコンポネントだと、“パイプライ ン全体の方向”という概念がハッキリしない。だがこの場合でも、絶対に“パ イプライン全体の方向”を決めなくてはならない。「AからB」でも「BからA」 でも、どっちでもいいからドチラカに決める! そうしないとJanusで採用して いる構成法や計算法が一切機能しなくなる。
繰り返すが、直感的に明確な意味があろうとなかろうと、「コンポネントの 入力と出力」「パイプライン全体の方向」は常に決定して意識しておく必要が ある。いいですね。
「パイプラインの図」のような、A, B, Cの直列結合は、数式的表現では、 A;B;C と書かれる。セミコロンが直列結合の演算記号だ。ところが、数学の伝 統的記法では、C・B・Aのように逆順で書くこともある。これだと図と向きが 反対になる。まれに、C・B・Aに合わせて右から左に向かって図を描く人もい る。上下方向になると、「上から下」と「下から上」が、ほぼ半々という感じ。 もう、注意するしか方法がないね。
最後にもう1つ。呼び出し(サービス要求)は、(我々の約束では)電流と同 様にプラスからマイナスに流れる。だが、これはコンポネント(箱)の外の呼 び出し弧の話で、コンポネント内部では、マイナスからプラスに呼び出しが起 きる。これも図を描いてみたら分かる。
細かい点や微妙な点については、説明を“はしょって”いるのだが、それで も、Janusの輪郭が多少は見えてきただろうか。ここまで読み通した方は、次 に予定されている、パイプラインの実例を述べた記事(*注12)や、Janusの基 本概念を整理した記事を読む準備ができたと思う。
Mon Feb 14 2005追記:記事「Janusパイプラインの例」にいくつかの例を示した。
Janusは、明確で一般性があるバックグラウンドを持っているから、プログラ ミング言語、論理、幾何、力学、形式言語理論などとの共通構造が知られてい る。それら隣接分野との交通路も開きやすい。 ところで、このサイトの「主要な話題」を読んだ人は、 「いったいこれのどこがXMLなんだ?」と思っているかもしれないが、とりあえ ずは、 「Janusのバックグラウンドは、構文論にも使えるのだ」と答えておこう。