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の実装

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と衝突しないことが保証されている。