Lispのマクロは、ソースコードをプログラムによって変換するものです。 マクロ変換器(macro transformer)が、ソースコードの部分木を受け取り、 加工したソースコードの部分木を返します。
伝統的なLispマクロでは、入力となるソースコードも、出力されるコードも、単なるS式でした。
Gaucheはそのタイプのマクロもdefine-macro
形式でサポートしています。
例えば、when
は伝統的マクロで次のとおり書けます。
(define-macro (when test . body) `(if ,test (begin ,@body)))
このマクロが(when (zero? x) (print "zero") 'zero)
のように使われたとすれば、
上記の変換器はそれを(if (zero? x) (begin (print "zero") 'zero))
と
書き換えます。一見問題なさそうですね。
けれども、begin
やif
が通常とは違う意味で束縛されている環境で
when
が使われたらどうなるでしょう。
(let ([begin list]) (when (zero? x) (print "zero") 'zero))
展開結果は次の通りになります。
(let ([begin list]) (if (zero? x) (begin (print "zero") 'zero)))
これでは意図した通りには動きません。展開された結果の中のbegin
が
ローカル変数と解釈されてしまいます。
これは変数捕捉の一形態です。Lispのマクロによる変数捕捉というと、
別の形態、すなわちマクロにより導入される一時変数がマクロに渡された式内の
変数を意図せずに捕捉してしまうことが話題に上ることが多いのですが、
そちらはgensym
を使って一時変数を決して衝突しない名前にすることで
簡単に回避できます。
しかし上の例のような変数捕捉はgensym
では回避できません。外側の
(let ([begin list]) ...)
の部分はマクロを書く人には制御できない
からです。マクロ作成者が、この衝突を避けるために出来ることは何もありません。
せいぜい、マクロ使用者がそんな使い方をしないように祈るだけです。
もちろん、begin
を再束縛するなんて誰もやろうとは思わないかもしれませんが、
同様の衝突はあなたのライブラリが提供するものも含めあらゆるグローバル変数について
起こり得るのです。
異なるLisp方言はそれぞれ異なる方法でこの問題に対処してきました。 Common Lispは、ある意味プログラマの常識に頼ります。マクロ作成者は ライブラリのパッケージを分けることで、偶然名前が衝突してしまう危険性を 減らせますが、マクロ使用者が同じパッケージの名前を再束縛することを防げるわけではありません。 (Common Lispの仕様ではCL標準のシンボルをローカルに再束縛した場合の 動作は未定義とされていますが、ユーザが提供するライブラリについては 何も決められていません。)
Clojureは、名前空間プレフィクスによって直接トップレベル変数を参照する方法を 導入したので、同名のローカル変数束縛をバイパスして意図するトップレベル変数を確実に 参照できます (また、Clojureのquasiquoteは高機能で、自由変数を自動的に プレフィクスつきのトップレベル変数へと変換してくれます。) この方法はローカルマクロが存在しない限りはうまくいきます。 ローカルマクロがあると、後の例で見るように、複数の同名のローカル変数束縛を 区別する必要が出てきます。Clojureの方法はローカル変数束縛とトップレベル変数束縛を 区別できるだけです。Clojureにはローカルマクロが無いのでそれでよいのですが、 Schemeは一様で直交する定理を重視するので、レキシカルスコープを持つローカル関数があるなら、 レキシカルスコープを持つローカルマクロもやっぱり欲しいわけです。
レキシカルスコープを持つローカルマクロを見てみましょう。説明のために、
ローカルなマクロ束縛を書けるlet-macro
という形式があると仮定します。
(実際にはlet-macro
形式はありません。マクロ変換器の指定方法が
やや異なるlet-syntax
とletrec-syntax
という形式があります。
ただ、ここではdefine-macro
と似たような形で例を示す方がわかりやすいので、
そのようなlet-macro
があるものとして説明します。)
(let ([f (^x (* x x))]) (let-macro ([m (^[expr1 expr2] `(+ (f ,expr1) (f ,expr2)))]) (let ([f (^x (+ x x))]) (m 3 4)))) ; [1]
ローカルな識別子mは、二つの式を引数として取り、S式を返すマクロ変換器に
束縛されます。従って、[1]の(m 3 4)
は
(+ (f 3) (f 4))
へと展開されます。上の式を展開結果を使って
書き直してみます (展開後はlet-macro
フォームはもはや必要ないので
展開結果には含めていません)。
(let ([f (^x (* x x))]) (let ([f (^x (+ x x))]) (+ (f 3) (f 4)))) ; [2]
さてここで問題です。展開結果に現れた[2]のフォーム内のf
は、どちらの
f
を参照すべきでしょう。上の式を文字通り解釈するなら、
より内側にある(^x (+ x x))
への束縛となります。
けれども、Schemeのスコープ規則にしたがえば、
外側のコードは、内側にどんなコードが来るかに関わらず意味が決まって欲しいわけです。
(let ([f (^x (* x x))]) (let-macro ([m (^[expr1 expr2] `(+ (f ,expr1) (f ,expr2)))]) ;; ここに書かれたコードがうっかり外側のコードに影響を与えてしまう ;; のは避けたい。 ))
マクロ作成者は内側のlet
がf
をシャドウしてしまうことを
知らないかもしれません(内側のフォームは他のコードをinclude
している
かもしれませんし、また他の人が、ローカルマクロが外側のf
を参照することに
気づかずに内側のコードを変更してしまうかもしれません。)
let-macro
の中に置かれるコードが何であれローカルマクロが動作するためには、
マクロの展開結果から「外側のf
」を確実に参照する方法が必要です。
基本的なアイディアは、
マクロ変換器m
により挿入される名前(f
と+
)に
「印」をつけて、二つのf
を区別するというものです。
例えば、フォーム全体を書き直して、対応するローカル変数がユニークな名前を持つように リネームしたらどうでしょう:
(let ([f_1 (^x (* x x))]) (let-macro ([m (^[expr1 expr2] `(+ (f_1 ,expr1) (f_1 ,expr2)))]) (let ([f_2 (^x (+ x x))]) (m 3 4))))
こうしておけばナイーブな展開でもスコープが正しく保たれます。つまり、
m
の展開結果に現れるf_1
は内側のf_2
と衝突しません。
(let ([f_1 (^x (* x x))]) (let ([f_2 (^x (+ x x))]) (+ (f_1 3) (f_1 4))))
(ラムダ計算において、レキシカルスコープを保ったまま高階関数を扱う際に 似たようなリネーム戦略を見たことがあるかもしれません)
上の例ではマクロの定義時に現れるf
(リネーム後はf_1
)が
マクロの使用時に現れるf
(リネーム後はf_2
) によって
シャドウされることを避ける話でした。
一方、もう一つのタイプの変数捕捉 (より頻繁に話題に上る、gensym
で回避できる捕捉)
は、マクロ使用時の変数がマクロ定義時に導入される束縛によりシャドウされてしまう
という問題です。これについても、同じリネーム戦略が使えます。
次の例を見てみましょう。
(let ([f (^x (* x x))]) (let-macro ([m (^[expr1] `(let ([f (^x (+ x x))]) (f ,expr1)))]) (m (f 3))))
ローカルマクロはf
の束縛を導入しています。
一方、マクロの使用時(m (f 3))
に、f
への参照が含まれています。
後者のf
は外側のf
を指すべきです。なぜならマクロを使っている
フォームは字句上、マクロ定義のlet
の外側にあるからです。
f
をレキシカルスコープによってリネームすれば次のようになるでしょう。
(let ([f_1 (^x (* x x))]) (let-macro ([m (^[expr1] `(let ([f_2 (^x (+ x x))]) (f_2 ,expr1)))]) (m (f_1 3))))
これだと展開しても二つのf
はきちんと区別されます。
(let ([f_1 (^x (* x x))]) (let ([f_2 (^x (+ x x))]) (f_2 (f_1 3))))
以上が、衛生的マクロの原理です (まあ、だいたいは)。
ただし、実際の実装では、すべてを一気にリネームすることはありません。
後者の例のようなケースで注意すべき点があります。後者の例では静的に
f
をf_2
にリネームしましたが、より複雑な場合にマクロ展開器が
再帰的に自分を呼ぶことがあり、その場合にはマクロの展開ごとに挿入されるf
を
別のものとして扱う必要があります。
従って、マクロの展開とリネームは協調して動作しなければなりません。
それを実装する戦略はいくつか考えられます。そして、Scheme標準は実装を どれかひとつの戦略に縛ってしまうことを良しとしません。 結果的に、標準はマクロシステムが満たすべき性質を、二つの簡潔な 文で示すに止まります:
マクロ展開器が識別子(変数かキーワード)の束縛を挿入した場合、 識別子はそのスコープ内で実効的にリネームされ、 他の識別子との衝突を避けられる。
マクロ展開器が識別子の自由参照を挿入した場合、その識別子は展開器が定義された場所から 見える束縛を参照し、マクロが使われる場所を囲むローカル束縛には影響されない。
これを読んだだけでは、これらの性質をいかにして実現するかは
すぐにはわからないかもしれません。そして既存の衛生的マクロ(syntax-rules
など)は
このいかにしての部分を隠しています。それが、
衛生的マクロをとっつきにくく思う理由の一つかもしれません。
これはある意味、継続に似ています。継続の仕様はごく簡潔に述べられていて、
最初に読んだときにはどう動くかさっぱりわからないかもしれません。
しかし経験を積んで使うのに慣れた後でもう一度元の説明を読むと、
必要十分なことが書いてあるとわかるのです。
この節ではいかにして衛生的マクロがこれらの性質を 実現しているかについての詳細には触れませんでしたが、 衛生的マクロが何をして、何のために必要かについて ある程度示せたのではないかと思います。 以降の節では、Gaucheがサポートする衛生的マクロシステムについて 例を交え紹介してゆきます。