catから始めるLimboプログラミング

IPWL(Inferno Programming with Limbo)の7章(チャネル)の途中まで目を通した。なんとなくLimboがどんな言語かわかり始めてきたかな。後は実際のコードを読み書きして、実践あるのみか。IPWLはそこそこの規模の例題が示されている点がよい。

ずっと前にLimbo版Hello Worldは書いたような気がするので、catのコードでも読んでみよう。

InfernoはPlan9やAlfeといった研究をベースに、Javaの登場にインスパイアされて生まれた実行環境(仮想マシンDis)+プログラミング言語(Limbo)+通信プロトコル(Styx)だけど、Limboはオブジェクト指向というアプローチは取らなかった(論文をたどれば理由は書いてると思うが)。その代わりモジュールプログラミングを採用している。モジュールはインタフェース定義とその実装にわかれていて、前者を.mファイルに、後者を.bに書くのが慣習になっている。

以下に/appl/cmd/cat.bを示す。モジュールの実装は「implement モジュール名」で始まる。

implement Cat;

include "sys.m";
include "draw.m";

include文はC/C++と違ってプリプロセッサではなく、コンパイラで処理される。ここではsys.mとdraw.mという二つのモジュールのインタフェース定義を読み込んでいる。Limboは強い型付け言語で、コンパイル時と実行時に型チェックが行われる。

Cat: module
{
	init:	fn(ctxt: ref Draw->Context, argv: list of string);
};

「Cat: module」がインタフェース定義。catのように短いプログラムだとこのようにインタフェース定義も.bファイルの中で記述される。initという関数があって、引数はctxtとargvで、それぞれの型は...というのがわかる*1

ちなみにInfernoシェルがコマンドを実行すると、モジュールがロードされて、initから実行されるというお約束になっている。

sys: Sys;
stdout: ref Sys->FD;

init(nil: ref Draw->Context, argl: list of string)
{
	sys = load Sys Sys->PATH;

	stdout = sys->fildes(1);

	argl = tl argl;
	if(argl == nil)
		argl = "-" :: nil;
	while(argl != nil) {
		cat(hd argl);
		argl = tl argl;
	}
}

init関数の先頭でSysモジュールをロードしている。Inferno/LimboではUNIX/Cのような暗黙的な静的リンク、動的リンクすることはなく、プログラマが明示的に利用するモジュールをロードする*2。まぁ、Javaスクリプト言語などはそれが普通ですが。使用するモジュールをロードし忘れた場合は、"module not loaded"と怒られて、Broken状態になる。

fildes()はSysモジュールの関数で、ファイル記述子をFDへのリファレンス(Cのポインタと考えればよいか)に変換する関数で、後に出てくるread、write関数はFDのリファレンスを引数に取る。FDはファイル記述子だけを持つADT (Abstract Data Type)だけど*3、なぜFDにラップして使うかというと、話はリソース管理とGCに関係してくる。Infernoにはcloseシステムコールが存在しない。FDが言語のスコープの外に出るか、明示的にnilが代入されるかすると、GCされるのだ*4

arglの型はlist of stringである。リスト操作はMLと同じで、hd(head)、tl(tail)、::(cons)が使える。便利。

cat(file: string)
{
	n: int;
	fd: ref Sys->FD;
	buf := array[8192] of byte;

	if(file == "-")
		fd = sys->fildes(0);
	else {
		fd = sys->open(file, sys->OREAD);
		if(fd == nil) {
			sys->fprint(sys->fildes(2), "cat: cannot open %s: %r\n", file);
			raise "fail:bad open";
		}
	}
	for(;;) {
		n = sys->read(fd, buf, len buf);
		if(n <= 0)
			break;
		if(sys->write(stdout, buf, n) < n) {
			sys->fprint(sys->fildes(2), "cat: write error: %r\n");
			raise "fail:write error";
		}
	}
	if(n < 0) {
		sys->fprint(sys->fildes(2), "cat: read error: %r\n");
		raise "fail:read error";
	}
}

catの本体は普通に読めると思う。「n: int」や「buf := array[8192] of byte」ってのはPASCALですな。raise文は例外を発生させる。

*1:ctxtはそのうち説明するかもしれないけど、取りあえずはお約束と考えておけばよい。

*2:モジュールシステムはMLの影響を受けたと書いてある。

*3:ADTはCLU由来。

*4:非同期I/Oがあるのか、明示的にフラッシュする方法があるのか?