explicit-renaming macroの拡張
今のところ、scheme界のlow-level(= Hygienicでないmacroも書ける) macroには少くとも3つの流儀がある。
- 伝統的なlispマクロ - defmacro, define-macro。Gaucheやbigloo、Gambit。
- syntactic-closureとexplicit-renaming - define-syntax。MIT/GNU SchemeやChicken、Chibi-scheme。
- syntax-case - define-syntax。ChezやKawa、Racket、R6RS Scheme全部(Guile, Vicare, Ypsilon, mosh, IronScheme, Larceny, ...)
実は、explicit-renamingはR6RSのSyntax-caseでも効率を無視すれば実装できるので、explicit-renamingはどこの実装でも使える共通のlow-level macroとしての可能性がある。
伝統的な3引数 explicit-renaming
explicit-renamingは特にChickenがネイティブのマクロシステムとして採用している。Chicken4.x系から、基本的にSyntax-caseのような他のマクロ実装は排除されている。(syntax-caseなシステムでは、define-syntaxの後に1引数の手続きを書くが、Chickenでは3引数の手続きを書く。)
3引数 explicit-renamingマクロは、transformerが3つの引数を取る。
- 式 - 変換対象の式
- compare手続 - identifierを比較する
- rename手続 - マクロを定義した環境での識別子を挿入する
explicit-renamingマクロでは、暗黙の束縛を導入したいときは、シンボルをそのまま出力に含める。
syntax-caseでのexplicit-renamingの実装
- https://github.com/okuoku/mosh/blob/8c0763af881c52e2d67e64b2c651f7f5e48cc3f0/lib/yuni/scheme/explicit-renaming.sls
- https://github.com/okuoku/mosh/blob/8c0763af881c52e2d67e64b2c651f7f5e48cc3f0/lib/yuni/scheme/unwrap-syntax.sls
syntax-case上でもexplicit-renamingは簡単に実装できる。compare手続きにはfree-identifier=?手続を使い、renameにはdatum→syntax手続が使える。
ただ問題は、syntaxをunwrapする必要がある点。syntax-caseは"必要になるまでidentifierオブジェクトの作成を遅延する"ようにデザインされているため、インクリメンタルなコンパイラには有利に作用するが、explicit-renamingのようなシステムを実装する上では不利になる。
↑のunwrap-syntax手続がsyntax-case上でexplicit-renamingを実装する上では必要になる。
ただし、nmoshやLarcenyでは、unwrap-syntaxは必要ない。これらのシステムでは、unwrap-syntaxの出力は常にもとの入力と一緒になる。
問題はもう一つある。このexplicit-renamingでは、シンボルを直接挿入することはできない。シンボルを挿入するかわりにsyntax-case固有のdatum→syntaxでidentifierオブジェクトを生成する必要がある。
2つのマクロシステムの違い
この違いが、そのまま2つのマクロシステムの違いとなる。つまり、identifierとsymbolを区別するか ? によって、syntax-caseとexplicit-renamingの互換性が損なわれている。(ただし、chibi-schemeはこれらを区別しているがexplicit-renamingを採用している。)
これらの違いを吸収するには、出力の段で違いを吸収する方法が考えられる。つまり、unwrap-syntaxの逆を行う手続を出力に通すことで、一応これらの違いを吸収することができる。
4引数 explicit-renaming
しかし、そのような対応はあまりクールでないのでexplicit-renamingを拡張することを考える。
つまり、シンボルを直接挿入するかわりに、暗黙の束縛を挿入するinject手続を加えた4引数のexplicit-renamingを考える。
4引数explicit-renaming+を使うと、いわゆるaifは次のように実装できる。
(define-syntax aif (er-macro-transformer+ (lambda (exp rename compare inject) (let ((it (inject 'it)) (pred (cadr exp)) (then-form (caddr exp)) (else-form (cadddr exp))) `(,(rename 'let) ((,it ,pred)) (,(rename 'if) ,it ,then-form ,else-form))))))
もし、伝統的なexplicit-renamingを採用したシステムでこれを実装するなら、inject手続きとして、単に(lambda (x) x)を渡せば良い。
ただし、↑の実装ではinjectを0引数で呼び出すとgensymを行うように拡張している。これは、gensymは単なるinjectに比べて最適化の余地が有る*1ことに依る。
*1:gensymによって挿入されたidentifierは、importしてきた他のidentifierと衝突しないことが保証されている。