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文は例外を発生させる。