プログラミング環境
目次
筆者が初めて UNIX に触れたのはもうずっと昔、そう日本に初めて UNIX が上陸した頃である。その時、もう UNIX 以外ではプログラムを書くまいと思った。それまでは所謂大型汎用機の世界にいた。実際その時以来大型汎用機では一度もプログラムを組む事は無かった。
現在 Plan 9 に触れて、もう UNIX ではプログラムを組みたくないと感じている。Plan 9 のプログラミング環境はなかなか良い。以下に幾つかの論点に渡って紹介して行こう。
Plan 9 には OS とアプリケーションの完全なソースコードが付属しており、それらの置き場所は man コマンドで見る事が出来る。またソースコードは UNIX のように ifdef や include の山ではなく概してすっきりと見やすい。UNIX でサポートはされているが、現在では価値の無くなった機能は大胆に捨てられている。認証関係やネットワーク関係のコードを別とすれば、一般的なアプリケーションのコードは驚く程簡潔である。
Plan 9 のシステム記述言語は C 言語と C のシンタックスを拡張した並列言語Alefであり、それらのコンパイラと気のきいたデバッガ acid が付属している。
C++ も存在するが Plan 9 のユーザはそれよりも Alef の方を好むであろう。もちろん多数の UNIX の標準ツール、grep, sed, awk, yacc, ... が使用出来る。それから emacs に代わる acme はプログラミングの好きな読者にはきっと気にいるであろう。acme は良く出来たエディタで emacs のように難しくはなく(多分初心者でもすぐに扱えるであろう)、それでいて emacs 並に強力である。
Library
Plan 9 のカーネルは C 言語で、アプリケーションは C と Alef によって記述されている。ライブラリのコードは両者で異なるものの、関数の名称や使い方は統一性が保たれている。従って一方で書いたプログラムは比較的他方へ移植し易い。Alef の紹介は後に回そう。ここでとり上げたいのはC言語の OS とのインターフェースやライブラリ関数である。(C 言語自体はどの OS でも基本的に同じだから特にとり上げる必要はない。)
一言で言えば Plan 9 でのプログラミングは UNIX でのプログラミングに比べて易しいのである。
UNIX は現代的に見て過去の遺物と化した多くのものをサポートしている。代表的な例が ioctl 関数 や fcntl 関数である。テレタイプなど過去の遺物をサポートする為の多くの機能がこれらの関数の使い方を複雑にし、ちょとしたことを難しくしている。そう、UNIX は端末インターフェースがテレタイプからスクリーンに移行しつつある時機に開発された OS なのである。従ってコンピュータをベースとする端末の特性が十分に考慮されず、様々な機能がパッチワークの様に追加され、IO 関数ライブラリは過剰サービスのために化け物と化している。
Plan 9 ははっきりとした割り切り方をしている。Plan 9 は raw モードとcooked モードの切り替え機能を持っている。そしてそれだけである。Plan 9 は cooked モードにおける加工機能の細かな設定は行わない。つまり単純化され特殊な加工が必要なら自分でやりなさいと言う立場をとっている。だから初等プログラマでも raw モードに入り込めるのだ。
Plan 9 の read 関数は実に気が利いている。UNIX の read 関数と異なり、この関数はバッファサイズのデータを必ず読み採らなければならないとは考えていない。通信ポートからのデータの読み取りや、raw モードの下でのキーの読み取りは、読み取れただけのデータを読み取り、抜け出してしまう。但し 1 文字以上のデータが到達するまで待つ。また cooked モードの下ではキーボードからのデータば改行コードを受け取ったタイミングで抜け出す。我々が行いたい事を知っているかの様にうまく処理してくれるのである。
この御蔭で例えば Plan 9 の日本語の文字コード変換フィルター tcs は、バッファリングをするのか否かをオプションとして指定する必要もない。プログラマは何も気にしないで read 関数を使って、それでいていろいろな状況下で巧く働くのである。
raw モードでの read 関数の仕様はキーボードの特性を強く意識している。昔のテレタイプと異なり現代のキーボードは1つのキーから複数の文字コードを発生させる。それ故プログラマは、受け取った文字列が異なるキーから別々に打たれたのか、それとも1つのキーから発生したのかを区別する必要に迫られている。Plan 9 の read 関数はこの区別を極自然になし遂げるのである。
Plan 9 ではシグナルの扱いも簡単化されている。シグナルマスクの概念は無く、捕捉関数だけでシグナルが料理される。UNIX のシグナルに関するあの複雑さはいったい何であったのかと考えたくなるくらいである。
Plan 9では sleep や alarm のコントロールを m 秒単位で行えるのも使い易い。UNIXでも実際上は m 秒単位のコントロールを迫られることが屡々発生する。しかしその場合にPlan 9の様に簡単には済まないのだ。
Plan 9 では /dev/kbd や /dev/mouse をアプリケーションから直接オープンしてプログラムを書ける。プログラマはこのようなデバイスからデータを読み取ればキーボードやマウスを扱えるであろうと直感し試してみるだろう。UNIX と異なり、Plan 9 のプログラマは自分の直感が正しい事を確信するに違いない。そう、Plan 9 ではこれらのデバイスがプロセス毎に多重化されていればこその話である。2 つのプロセスがこれらのデバイスを読み取っても何の問題も発生しない。
Plan 9 では fork 関数が拡張されている。fork(void) は rfork(int) の特殊ケースだ。
UNIX では fork によって分離されたプロセス相互で共有変数を使用出来なかった。もしもその事が必要なら、一方のプロセスで変数の変更が発生した事をシグナルで他方に知らせ、pipe を通じて他方にデータを渡さなければならなかった。この事はプログラムを著しく複雑にするばかりか、シグナルが受け取られる保証が無いために、プログラムの信頼性を著しく損なうのである。嬉しい事にPlan 9 の rfork は共有変数を実現する。
些細な事かも知れないが、Plan 9 の C プログラムは include 文の山にはならない。Plan 9 のライブラリは良く整理されており、ヘッダファイルはライブラリ毎に存在するのでヘッダファイル自体が少ないのである。通常のプログラムは u.h と libc.h だけで済んでしまう。これ以外に必要になるのは UTF-8 コードを扱う場合、グラフィックスを扱う場合など特殊な場合だけである。
Plan 9 のライブラリ関数は UNIX の様に多くは無い。サポートが悪いと言う意味ではない。Plan 9 には UNIX でサポートされていない多くの機能がサポートされている。
UNIX のライブラリ関数はよろず屋である。細々とした機能がライブラリとしてサポートされている。他方 Plan 9 は本当に必要なもの、本当に良く使われるものだけがサポートされている。例えば、Plan 9 にはパスワード入力で要求されるエコーの無い読み取り関数を持っていない。この機能は特殊であり容易に作れるから自分で作りなさい、との立場をとるのである。
筆者はこうした問題を解決する為に、ソースレベルのライブラリを持っている。必要ならそこから切り張りしてプログラムにくっつけるのである。もっとも OS のソースコード自体が最も豊かなソースレベルライブラリと言えるが...
Alef
Alef はC言語に似た Plan 9 の並列言語である。並列機能を必要としなくても Alef は C に比べて使い易い言語であり、C ではもうプログラムを書く気がしなくなるであろう。例えば C の struct を考えてみよう。プログラマは struct を引きずりたくはないだろう。そこで実際上は typedef でこの問題を処理する。
struct Point { int x,y; }; typedef struct Point Point; main(){ Point p; p.x = 1; p.y = 2; ... }Alef はプログラミングスタイルのそのような現実を認め、typedef で再定義しなくてもよいように出来ている。
aggr Point { int x,y; }; void main(void){ Point p; p = (1, 2); ... }そして嬉しい事には ( ) で構造化データを一挙に渡せるのである。この( ) は関数引数としても使えるし、代入文の左辺にも使える。
p = (Point){1, 2};で代入可能である。なお Plan 9 の Cコンパイラはこの代入を可能にしている。
C 言語では既存の struct タイプに変数を追加した場合に、その参照の仕方がどんどん複雑になって行く。例えば Point から Circle を構成した場合を考えてみよう。
struct Point { int x, y; }; typedef struct Point Point; struct Circle { Point p; int r; }; typedef struct Circle Circle; main(){ Circle c; c.p.x = 1; c.p.y = 2; c.r = 10; ... }このプログラムは Alef では以下の様になる。
aggr Point { int x, y; }; aggr Circle { Point; int r; }; void main(void){ Circle c; c = ((1,2),10); ... }最後の c への代入は
c.x = 1; c.y = 2; c.r = 10;と書いてもよく、C 言語の様に c.p.x などと書く必要がないのである。さらに座標だけへの代入を行いたい場合には単に、
c.Point = (1,2);で済む。
つまり Alef では構造化データの宣言の中に変数名を指定しないデータタイプを書く事が可能で、その場合、その構造が継承される。データ構造の継承は OOP(Object Oriented Programming) の基本的なテーマであるが、C 言語の struct は(時代的な背景もあり)その点での整合性を欠いているのある。
Alefの aggr は Alef の抽象データタイプである adt と良く整合している。と言うよりも、adt のシンタックスは aggr の自然な拡張になっている。adt ではC++の class と同様にメソッドを定義できる。またデータやメソッドの継承が可能である。他方では C++ の class の様に operator の宣言はできない。またコンストラクタやデストラクタも使えない。それらの点を除けば adt は class と同等な機能を持つ。
adt は C++ のclass の様に難しくはない。adt の使い易さは、欲ばらずあくまで aggr の延長上に、
- 構成要素
- 要素の継承関係
- メソッドの機能
- アクセス制限
例を示そう。(実用性に関して深刻に考えないで欲しい。)
#include <alef.h> aggr Point{ int x,y; }; adt Lattice{ Point; void print(*Lattice); void init(*Lattice, int, int); Point value(*Lattice); }; void Lattice.print(Lattice *this){ print("(%d,%d)\n", this->Point);} void Lattice.init(Lattice *this, int x, int y){ this->Point = (x, y);} Point Lattice.value(Lattice *this){ return this->Point;} adt Gauss { Lattice; void print(*Gauss); }; void Gauss.print(Gauss *this){ print("%d+%di\n",this->Lattice.value()); } void main(void){ Lattice a; Gauss b; a.init(10, 15); a.print(); b.init(20,25); b.print(); }この例では Gauss は Lattice を継承している。そのことはGauss の構成要素として Lattice が指定されている事から分かる。Gauss は Lattice の print だけは継承せず新たに定義する。このことは Gauss の構成要素として print が指定されている事から分かる。
adt ではデータ部は原則非公開、関数部は原則公開である。この原則は extern と intern の修飾子で変更できる。この例の様に Point に対して代入と参照を許すのなら、 実は Point を extern で修飾した方が簡単なのである。
adt の宣言の中で書けるのは関数の宣言であって定義ではない。定義は外で行う。
関数宣言の中の *Lattice の様に '*' に型名が続いている第一引数は、関数の使用時に省略される事を意味している。またそれは関数定義との関係では意味的には Lattice* である。
最後のちょと分かりにくい説明を除けば他の全ては aggr と関数定義の自然な拡張で何も新しいものは無い。なおここに現われる this は予約語ではなく(C++に親しんでいる読者の為に)筆者がかってに選んだ名前である。筆者は普段は Objective-C 風に self を使っている。
Alef の adt と C++ の class は似た面も持っているが、目指されているものが異なっている。Alef の adt はオブジェクトの構成要素に目を向ける。他方 C++ はオブジェクトの継承関係(クラス分類)に目を向けるのである。
OOP の教科書にはクラス分類が世の中の自然な姿であるように描かれているだろう。そしてそれを示す様々な例が持ちだされる。だがクラス分類は認識の終着点であり、出発点ではないことに触れているだろうか? しかも認識の終着点などと言うものは実際上存在しないのである。
- Stephen, etc,「C++ 言語入門」(アスキー出版)
を参照するがよい。PinsonはOOPにおける設計法の解説を含んでいる。
またプログラミングパラダイムの変遷とOOPの到達点に関する解説としては
B.Stroustrup,"What is Object-Oriented Programming ?"
がある。(簡単に手に入ると言う意味で載せた。)
19世紀の半ば、世界最初の計算機の実現の為に悪戦苦闘しているバベッジを想像してみよう。彼は自分の計算機械が歯車式計算機のクラスに位置付けられると考えたであろうか? 彼にとってはクラス分類などはどうでも良い事であり、自分の新しい機械を構成する要素は何であるか、それらがどのように有機的に機能すべきかに思考を集中したに違いない。現在我々が行う彼の計算機の位置付けは、バベッジの時代からずっとずっと後になって、電子計算機を知っている後世の人間が考えた事である。それは不変な分類ではなくて、将来は別の角度から分類される可能性を持っている歴史の中の一コマに過ぎない。
我々は新たに設計したオブジェクトの親を決めるルールを持ち合わせていない。そのようなルールなどある訳がないのだ。読者は自問自答してみるがよい。「私は父親の派生クラスなのか、それとも母親の派生クラスなのか?」って。C++ はこのような意地悪な問いかけに対して多重継承なるものを準備している。しかしこれは言わば非常手段なのであり、C++ は多重継承を嫌うのである。C++ に限らず OOP なるものはオブジェクトの継承の木(進化図)なる壮大な空間の中に個々のオブジェクトを位置付けようとする試みなのであろう。しかしそれは大変に難しい事であり、プログラマにプログラムの実現には不要な他の能力を要求するのである。(筆者はかような能力を持ち合わせていない。)
Alef は並列言語として C 言語や C++ には無い機能を持っている。C と同様 fork が実行出来るが、その他に(Alef の言葉で言えば) process と task が生成できる。
Alef の process は UNIXの(正確にはMach OS の) thread に相当する。(以下プロセスとprocessを区別する。) 即ちプロセスIDを持ち、CPU 資源の割り当てを待つ並列プロセスであるが新たなメモリ空間を生成しない。UNIX の C 言語の中で thread を扱うのと異なり、言語機能としてサポートされているのでプログラムはうんと読み易くなる。
Alef の task は process の構成要素であり、1つの process の中で複数の task がイベントの発生を待つことができる。マウスベースで動作するアプリケーションを考えてみよう。幾つかのボタンがあり、マウスによるボタン操作に従ってアプリケーションは何かの動作を引き起こす。このような問題に対してボタンの1つ1つに対して process を対応させる必要は無い。2つのボタンが同時にクリックされる事は無いし、1つのボタンがクリックされた場合にはその処理が終わるまで次のボタンのクリックに伴う処理を始める訳には行かない。即ち並列処理の必要性は全くないのであって、処理を行う1つのプロセスが存在すれば足りるのである。Alef の task はこのような問題に対して効果的に対処してくれる。プログラムコードの外見は process と似ているのだがシリアルに処理を行うイベント処理に使われる。
task や process は channel を通じてイベントの発生を伝えあう。Alef は task や process がイベント処理を行う時に発生するクリティカルな問題を解決する為の言語レベルでの様々な機能を持っている。