R7RS-largeではExplicit Renaming(以下ER)が入ると噂されている。まだSRFIすら出ていないのでいつになるのか全く不明ではあるのだが、それがどのように動くかを理解しておけば近い将来(と信じたい)ERが入った際に慌てふためくこともないだろう。といっても
syntax-case
より遥かに簡単なので身構える必要もないのではあるが。
簡単な使い方
まずは簡単な使い方を見てみよう。例として
let
をERで書いてみることにする。名前付letは考えないことにするのでマクロの名前は
my-let
としよう。また、簡便にするため既存の
let
を使うことにする。
(import (scheme base))
;; import er-macro-transformer
(cond-expand
(gauche (import (gauche base)))
(sagittarius (import (sagittarius)))
(chibi (import (chibi)))
(else (error "sorry")))
(define-syntax my-let
(er-macro-transformer
(lambda (form rename compare)
;; form = (let bindings body ...)
;; bindings = ((var val) ...)
(let ((bindings (cadr form))
(body (cddr form)))
`((,(rename 'lambda) ,(map car bindings) ,@body)
,@(map cadr bindings))))))
ER展開器は3つの引数を取る手続きを受取りマクロ展開器を返す。それぞれ入力式、リネーム手続、比較手続の3つである。ERで最も重要なのは二つめのリネーム手続で、この手続きによってマクロの健全性を保証することができる。上記の例ではマクロ内で
lambda
をリネームすることによって、以下のような場合でも正く動作することを保証することができる。
(let ((lambda 'boo))
(my-let ((a 1) (b 2)) (+ a b)))
;; -> 3
ERでは健全性、入力式のチェック等は全てユーザに委ねられる。
syntax-case
と異りデフォルト(リネーム手続きを呼ばない単なる式変形)では非健全である。
どうやって動いてるの?
ERの動作原理は実に単純である。リネーム手続きはシンボルをマクロ定義時の環境から見える識別子にして返す。例えば上記の例では
my-let
が定義された際に見える識別子というのは
(scheme base)
からエクスポートされているものになる。これは
lambda
を含むのでリネーム手続きにシンボル
lambda
を渡すと
(scheme base)
からエクスポートされている
lambda
を指す識別子を返してくる。
;; very naive/simple image
;; environment when my-let is bound
;; ((exporting-name . actual-binding-name) ...)
'((lambda . |lambda@(scheme base)|)
... ;; so on
)
;; very naive implementation of rename procedure
(define (rename s)
;; (macro-environment) returns the above environment
(cond ((assq s (macro-environment)) => cdr)
(else
;; convert to identifier which is bound in macro-environment
#;(implementation-dependent-procedure s)
)))
ここではリネーム手続きはシンボルのみを受け取ると仮定しているが、処理系によってはフォームを受け取ることも可能である。特に仕様が決っていないので処理系独自に拡張されているが、ERをサポートしている多くの処理系ではフォームを受け取ることが可能である。
追記:
リネーム手続きに同名のシンボル又は識別子を渡すと常に同一の識別子が返って来る。同一のオブジェクト(
eq?
で比較した際に
#t
)ではない可能性に注意してほしい。R6RSの語彙を借れば
bound-identifier=?
で
#t
を返す識別子である。
(define-syntax foo
(er-macro-transformer
(lambda (form rename compare)
(list (rename 'a) (rename 'a)))))
(foo)
;; -> list of the same identifiers in sense of bound-identifier=?
(let () (foo))
;; -> list of the same identifiers as above macro expansion
;; because input of rename is always 'a
「Hygienic Macros Through Explicit Renaming」によればリネーム手続きの呼出毎に
eqv?
で
#t
を返す識別子が新に作られるが、多くのR7RS処理系(Gauche、Chibi、Sagittarius)では同一のオブジェクトを返すようになっている。もちろんそれに依存したコードを書くと後で痛い目にあうかもしれない。
比較手続き
ERが受け取る3つ目の手続きは比較用手続きは渡された2つの入力式が同一であるかを比較する。ここでいう同一とは、同一の束縛を指す。言葉で説明するよりも例を見た方が早いので先ずは例を見てみよう。
(define-syntax bar (syntax-rules ()))
(define-syntax foo
(er-macro-transformer
(lambda (form rename compare)
(define (print . args) (for-each display args) (newline))
(print (compare (cadr form) (rename 'bar))))))
(foo bar)
;; -> #t
(foo foo)
;; -> #f
(let ((bar 1))
(foo bar))
;; -> #f
bar
は束縛されている必要はないのだが(なければ未束縛として比較される)、ここでは話を簡単にするために敢えて束縛を作っている。
foo
は一つ目のフォームとリネームされた
bar
が同一の束縛を指すかをチェックするだけのマクロである。
一つ目の(foo bar)
では与えられたbarは大域に束縛されたbar
と同一の束縛を指すので#t
が返る。これは以下のようにした際に参照される束縛と同一のものと考えれば自明である。
bar
;; -> syntax error
二つ目の
(foo foo)
は
bar
ではないので
#f
が返る。
三つ目の
(foo bar)
は引数
barがマクロ展開時に見える束縛と異る為に、この場合は局所変数
barが見える、
#f
を返す。
R6RSの語彙を借て説明すれば、比較手続きは
free-identifier=?
とほぼ同義である。(処理系の拡張によってフォームを受け取れる可能性があるため「ほぼ」としている。)
比較手続きを用いることで
cond
の
else
などの補助構文の導入が可能になる。
まとめ
非常に簡潔にだがERの使い方及び動作モデルの説明をした。
syntax-case
でもそうだが、ERも付き詰めればマクロ定義時と展開時の識別子が何を指すのかというところに落ち着き、ERではこれを非常に明示的に記述することができる。
参照
William D Clinger - Hygienic Macros Through Explicit Renaming
https://groups.csail.mit.edu/mac/ftpdir/users/cph/macros/exrename.ps.gz