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