Scheme は比較的オーバーロードの少ない言語で、標準の手続きでオーバーロードが使われているのは数値計算とポートまわりくらいだろうか。
しかし、構文レベルで見ると非常に大きなオーバーロードがある。括弧だ。
こんな指摘がある。
全くの初心者にschemeをやらせた経験では、文法を理解するのに苦労するみたいだった。引数リストの()とcondの条件節の()と関数適用の()……等々がみんな同じ見た目(だけど解釈がそれぞれ違う)だから何がどうなってるのかわからなくなる。
— むとう (@masa_edw) July 14, 2011
関数定義するときの (define (func args...) ...) の(func ...)は関数呼び出しではない! なにそれ! (cond (c0 value0) (c1 value1) ...) の (c0 value0)は関数呼び出しではない!
— むとう (@masa_edw) July 14, 2011
つまりどのカッコが構文の一部でどのカッコがただの式(評価されて値になる)かっていうことと、どの式がいつ評価されるのかっていうのが、見た目の違いがないのでわかりにいうということですね。defineの次は構文、condの各節も構文、ifの二つ目は式だけど評価されないときもある、等々。
— むとう (@masa_edw) July 14, 2011
意味によって括弧の見た目を変えると読みやすくなるのだろうか。試してみよう。
実装戦略
ではどのように実装するか。
括弧の意味を確定させるためにはプログラム中のマクロをすべて展開しなければいけない。 RnRS Scheme には Common Lisp Hyperspec Sec. 3.1.2.1.2.1 のような special form の定義はないし、そもそも macroexpand
が存在しないので、どうしても処理系依存にならざるをえない。今回は Gauche を対象にする。
マクロ展開を真面目に追おうとすると、 macroxpand
だけでは足りず、現在可視な束縛やモジュール、ローカルマクロも追わないといけない。他によい方法があるかもしれないが、とりあえずコンパイラに手を入れることにしよう。
括弧の種類をどこに保存するか。 read
したソースコードをキーにした連想リストをグローバルに持ち assq
で引くのがポータブルそうだが、あまりグローバル変数は作りたくない。必要なところまで引数で渡すのも面倒だ。 Gauche には(undocumented だが) extended pair というものがある。 Gauche のソースコードは extended pair になっていて、そこにソースプログラムのファイル名や行番号などが入っている(src/read.c の read_list
関数などを参照)。表示部分でどうせ行番号が必要になるはずだから一緒に入れてしまった方がきっと便利だ。
src/compile.scm の pass1
を見ると、リストの car
を見ながら、順番にプログラムを中間形式に変換している。ここで渡ってきたリストに順番に属性をつければよさそうだ。入力のプログラムを意味もなく deep copy するようなお行儀悪いマクロがあると情報が途切れてしまうが、どうにもできないので無視する。
改造した pass1
を呼び出し、その後種別情報をS式で出力するプログラムを書く。下のように、ファイル名と行番号、リストの先頭のシンボル、種別(macro
または procedure
)くらいがあれば色付けするには十分だろう。
(("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 34 "define-module" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 35 "use" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 36 "export" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 51 "select-module" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 76 "define-macro" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 77 "define" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 78 "let" macro)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 78 "gensym" procedure)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 79 "gensym" procedure)
("/usr/local/share/gauche-0.9/0.9.4/lib/gauche/dictionary.scm" 80 "gensym" procedure)
...
)
表示は、これを Emacs で読み込んで適当に色をつける。
できたもの
ということで、こんなものができた。
Gauche は 0.9.4 にこんなパッチを当てた。
Emacs 側では jit-lock
で適当なタイミングで括弧の種類をS式で吐くプログラムを呼び出し色をつける。種別情報はマクロか手続きかだが、それ以外の部分にも薄い色をつけることにした。行単位でしか情報を持っていないのでたまにハイライトに失敗することがあるかもしれない(Gauche の内部的にはバイト単位のオフセットもあるが、それを Emacs の文字単位のオフセットに変換する方法がよくわからなかった)。
感想
あまり深く考えずにナイーヴに実装してみたがそれなりに動いている。
で、実際に色がついてプログラムが読みやすくなったか、ということだけれど、個人的にはよくわからない。S式を見るときは、普段は特に括弧は意識せず、必要になったときだけ括弧を見ているので、色がついて自己主張されるとそれはそれで鬱陶しい気がする。と言いつつも、 let-values
のような括弧がちな構文や、 match
のパターン部分のような、構文の括弧の中にときどき評価される式が混ざる場合は確かに読みやすくなる気がする。