Think Stitch
PRINCIPIA  最近の更新


相互作用と計算の分離

コンピュータは日本語で計算機といわれるとおり、何か有用な計算をするために使われます。 例えば数値計算や、ソート、探索などです。 このような計算をするプログラムは、入力を与えられるとガチャガチャ(と今はいいませんが)と動いて結果を出して止まる、というものでした。 図示すると次のような感じです。数学の関数と見なせるようなプログラムです。

出力をしたら計算機が止まる、というプログラムはもうないと思いますが、近い形のものは残っています。 例えばコマンドラインから実行されるプログラムには近いものがあります。 実行されると標準入力やファイルからデータを読み込み、計算を行い、そして結果を標準出力やファイルに書き出して停止するものです。 このようなプログラムの仕事のほとんどは、計算そのものを実行することでしょう。 (途中で対話的に入出力を行うものを除きます。)

一方、コンピュータを利用した最近の機器のほとんどは、計算そのものが主目的ではないでしょう。 もちろん内部では計算を大量に行っているでしょうけれど、ここで注目したい差異は、計算して終了するというようなものではないという点です。 携帯デバイスも、家電に組み込まれたマイコンも、データセンターに設置されているサーバーも、みな常に稼動していて、ユーザや他のデバイス、コンピュータと継続的に相互作用を行います。 このように他者と継続的に相互作用を行いながら仕事をするシステムをリアクティブシステムといいます。 図示すると次のような感じです。

リアクティブシステムに対して、前の計算だけを行うシステムをここでは計算システムと呼ぶことにします。

ここで考えたい問題は、リアクティブシステムのプログラムの作り方は、計算システムのプログラムの作り方とどのように違うのか、あるいはどのように作るといいだろうか、という問題です。 この問題を少し考えてみたいと思います。 リアクティブシステムのプログラム、計算システムのプログラムではちょっと長いので、リアクティブプログラムと計算プログラムと呼ぶことにします。

リアクティブプログラムの仕事

リアクティブプログラムがやらなければならない仕事が3つあります。

1つはもちろん計算です。 例えば相互作用の相手に渡すデータを計算する必要があるでしょう。 ユーザに見せる3Dグラフィックスの計算も、これに含まれると考えられます。

もう1つは相互作用そのものです。 つまり入出力です。 ネットワークを通じた通信であるとか、ボタン押下の検出であるとか、ディスプレイへの出力などです。 感覚的ではありますが、これらは「計算」とは違うものだと思えます。 計算システムと異なり、これらは継続的に繰り返し行われます。

最後は状態の保持と管理です。 いまどの状態にいるのか、次はどの状態へ行くのか、ということです。 状態の表現によっては複雑な計算を必要とする場合があります。 例えばデータベースを内包しているシステムでは更新の計算を行う必要があるでしょう。 この計算は1番目の仕事に含まれます。 しかしデータベースそのものを保持することは、状態の保持です。

計算プログラムにだって、計算途中での状態があります。 しかしリアクティブシステムの状態には、これとは質的に異なるものが含まれています。 それは相互作用をしている相手がどういう状態にいるのか、という想定です。 リアクティブシステムの仕事は、相手との継続的なやり取りによって進んでいきます。 仕事を行うにはあらかじめ定められた手順に従って行う必要があります。 ご存知の通り、この手順はプロトコルと呼ばれています。 手順を進める上で、各時点において相手に次に何ができるのか、何を期待できるのかを判断する必要があります。 そのためには相手のいる状態、プロトコルで定められた範囲の詳細度での相手の状態を覚えておく必要があります。 例えば、あるデータの要求はネゴシエーションが済んだ後に可能になるとプロトコルで定められている場合、お互いがネゴシエーション完了後であることを認識している必要があります。

より根源を探れば、継続的に相互作用をして仕事をするということは時間の経過があるということで、 時間の経過があるということは、時々刻々変化する相としての状態があるということです。 そのような主体が2つ以上あって、相互理解を行おうとすれば、必然的に相手の状態を想定する必要があるということです。 リアクティブシステムがそのようになるのは、相手にしている対象が物理的な世界にあるからだと思います。 ええと、ちょっと話が飛びすぎました。 要するに、リアクティブシステムでは状態というものを扱うことが必然だということです(あとでちょっと例外に触れます)。

計算プログラムとプログラミング言語

計算システムの持つ状態はリアクティブシステムのものとは異なり、自分だけで勝手に決められるものです。 入力と出力の関係だけが満たされていれば、途中がなんであってもかまわないのです。

話は変わって、一般的な命令型のプログラミング言語、手続き型、あるいは関数型と呼ばれているものも、どれも計算プログラムを書くのにとても合うように作られているように思います。

特に関数型プログラミング言語は計算システムにぴったりです。 計算の途中結果以外に状態を持たなくてもよいので、非常に見通しよくプログラムを作ることができますし、検証も容易です。 名前の付いていない計算の途中結果や制御の位置を状態として認識することはほとんどないでしょう(特に遅延評価の場合は)。

命令型、あるいは手続き型(これは命令型のバリエーションの1つという認識ですが)の場合は明示的(という言葉はなかったそうですが)にデータ構造としての状態を持つことになります。 それでもなお、計算プログラムを記述するのに適しています。 関数型プログラムが途中の状態を気にしなくてよいのに対し、命令型の場合は計算の途中過程に名前をつける必要があります。つまり変数ですね。(しかもこれを使いまわすからややこしいことになるわけです。これはおかしな言い方に聞こえるかもしれませんが、関数型から見ればそうもいえるでしょう。加えて変数を経由した計算の流れが複数混在して離れたところに置くことができ、字面からはつながりが見えにくいのでさらにややこしくなるのです。)

乱暴な言い方になりますが、いずれの場合もプログラミング言語の要素が計算プリミティブとそれを連結する糊の役目をするものからできていて、それらを連結していくと計算プログラムになります。 計算システムの状態は、一部を変数が明示的に、残りを制御の位置が暗黙的に保持しています(プログラムカウンタの値という意味では変数ですが)。 計算の流れは言語による記述の流れとほぼ一致していてます。 これがプログラムの書きやすさと理解のしやすさにつながっています。

リアクティブプログラムの記述

ではリアクティブプログラムはどんなふうになっているのかという話です。 何か網羅的な調査結果でも出るのかと期待されると困ります。 何も出ません。 イベント駆動方式(イベントドリブン方式)の話だけします。

その前にちょっと脱線して、意外に思うことが2つあります。 1つはリアクティブシステムに適した設計の言語はなぜ普及していないのだろうかということです。 きっといろいろあった(ある)のだと思いますけど、普及した言語+相互作用のハイブリッドには勝てなかったんでしょうか。 もう1つは、初期の計算モデル、計算機の概念モデルであるチューリングマシンは、計算システムよりもリアクティブシステムに似てますよね。ただそれだけなんですが。

イベント駆動方式

リアクティブプログラムを作るのに、イベント駆動方式はよく使われていました。いまはどうなんでしょうか。

イベント駆動方式は細かく分ければいろいろあるのでしょうけれど、状態を保持し、イベントを受けたらハンドラを実行して計算を行い、なんらかの出力を行った後に状態を更新するというものです。 ここにはリアクティブシステムの3要素が含まれています。

これを手続き型のプログラミング言語、例えば C言語で書くとかなり見通しが悪いですよね。 いちばんいやなのは、計算プログラムと違って頭の中にある動作のイメージとプログラムの記述とで形が違うという点です。 頭の中には状態遷移図とか、あるいはシーケンス図のようなものがありますが、イベント駆動方式では全部ハンドラにばらされて平たく並べられてしまうので、理解しながら追いかけるのがたいへんです。 状態遷移だろうがプロトコルだろうが、記述上には流れなんか現れないのですから。 書くのもとてもたいへん、修正するのはさらにたいへんです。 ここは自動生成ツールとか、自前のツールとかを使う人が多かったろうと思います。 ここだけは E×CEL 使っても許してください。

そんなにたいへんならば、なぜその書き方が使われるんでしょうか。 たとえば状態の位置を制御の位置で表すことはできないでしょうか。 やればできますよね。全部 goto にしてしまえばいいのです。 石が飛んできそうですが、全部 goto にした方がむしろわかりやすいのではないかと思います。 すくなくともプログラムの記述の構造が、状態遷移図と一致します。 複数の状態で共通にあるイベント処理は、継続を表すトークンなどを導入すればまとめることができるでしょう。 結局、イベント駆動(の実装方式の1つ)をややこしくしているのは、手続き呼び出しから返らなければならないという点にあるように思えます。 処理の再利用として導入された手続きという概念を、あまり合わない対象に適用しちゃったという感じです。

オブジェクト指向プログラム

規模の大きなリアクティブシステムを単一のイベント駆動プログラムとして実現するのはさらにたいへんなことになります。 多段階のディスパッチ表を使うような設計を見たことがありますが、誰がうれしいのかわかりません。

こういうときは例によってコンポーネントに分割するわけですが、ここで問題になるのはコンポーネントをどういった種類のものにするかということです。 こういうときに使える一般的な原理の1つは、分割する前と同じ種類のものにしろ、というものです。ある種の要素を合成した結果が、また同じ種のものになるという性質を閉包性というのだそうです。 (閉包性という言葉は別の意味でも使われるので注意が必要ですが、より抽象的なレベルでは同じになると思います。) 代数学なんかはそうですが、こうすると概念が少なくて済むし、分析もしやすいし、どちらの方向にも性質が継承できることが多いので、よいことがたくさんあります。 オッカムさんもマッハさんも喜びます。 いまの場合は、コンポーネントもリアクティブシステムにする、ということです。

それぞれが独立して動くコンポーネントからなり、それらが相互作用しつつ全体として有用な仕事をするるシステムのことを並行システムというのでした。 プログラムの世界では、やや抽象的な視点でのオブジェクト指向プログラムということになると思います。 オブジェクト指向の話をするとこわい人がたくさん来ると聞いているのであまりしたくないのですが。 GUI アプリケーションは典型的なリアクティブプログラムだと思いますが、その多くはオブジェクト指向的に作られているのだと思います。 たとえオブジェクト指向言語が使われていなくてもです。 GUI を構成する部品や、アプリケーションのロジックを実行する計算主体の部分もみなオブジェクト(小さなリアクティブシステム)として実現されます。

一般的なオブジェクト指向言語の場合(C++ や Java を想定しています)、状態はメンバ変数で保持し、相互作用はメンバ関数の呼び出し、計算はメンバ関数の中で(想定言語の場合)手続き的に行います。 たしかにリアクティブシステムの3要素があります。 ただしここでちょっと気になるのは相互作用としてのメンバ関数呼び出しです。

相互作用の実現方法

命令型のプログラミング言語(これはオブジェクト指向とは直交する概念だと考えています)でリアクティブシステムとしてのコンポーネントを実装する場合、相互作用の実現方法には大きく分けて2つの方法があると思います。 1つは制御を渡す方法、もう1つは共有変数を使う方法です。 ここではマルチスレッドのことは考えていません。共有変数の場合は、1つの制御をどこかで切り替えて使いまわすと考えます。

どちらの場合も困ると思うのは、それが計算に属するものなのか相互作用に属するものなのか、言語要素の型からは判断できないということです。 変数の場合には状態なのか相互作用の媒体なのか区別できません。 場合によってはどちらとも判断がつかないようなものもあります。 設計の段階では概念的に分離しておいても、実装の段階や修正の段階で混じってしまうこともあります。 このことは、オブジェクト間での相互作用に注目するという場合やオブジェクトのプロトコルを検証するという場合の障害になります。 すくなくともプログラムの記述の中から、相互作用に関する部分だけを抽出する、というようなことができないことは明らかでしょう。 関数呼び出しを相互作用の手段として使うという選択には別の問題もあるのですが、これについては別の機会にしたいと思います。

相互作用と計算の分離

リアクティブシステムに状態、計算、相互作用の3要素があるのならば、記述においてもそれらを明確に分けた方がいいだろうと思うのです。 仕様を含む設計においても、コードを記述するという意味での実装においても、相互作用と計算を分離しておくことは利点があります。 相互作用については、SyncStitch でできるように、それだけに注目して記述したり検証したりできます。 計算については、何から何を計算すればいいのか明確にできますから、計算プログラムのように明確に規定することも記述することもできます。 計算の記述はどのような計算原理でもかまいません。 SyncStitch のように関数型にすれば記述も検証も容易です。 命令型を使うにしても、テストや検証の方法はたくさんありますし、相互作用と分離されていればかなり容易になります。 そういう意味で CSP は優れていると思うのです。

補足

  • 喜ばしいことに様々な関数型プログラミング言語が広まってきています。並列化コンパイラや並列計算のための言語要素を持っているものもあるそうです。しかし、それとリアクティブシステムの話は別です。リアクティブシステムは状態を持たなければならないので、すべて関数型にするというわけにはいきません(以下の項参照)。これは関数型か CSP かという選択ではないということです。並列化が可能で型安全で整備されたライブラリを持つ関数型言語を CSP の計算部分に使えばよりよいということです。設計の初期段階で、フットワーク軽くモデルを作って試行する場合はまたちょっと別とは思いますが。
  • 実は純粋な関数型でもリアクティブシステムを作る方法があります。ストリームを使う方法です。リアクティブシステムやプロトコルの振る舞いを規定する際に、状態を使うのではなく、ありうるすべての相互作用の系列を指定するという方法があります。これはストリームの考え方にも似ていますし、CSP のトレース意味論にも似ています。詳しくは SICP をご覧ください。残念なことに今日、日本語訳絶版のニュースが流れてきましたが。閉包性の話も SICP に少し出ていたと思います。
2013/08/07
© 2013,2014,2015 PRINCIPIA Limited