Hygienic macros.
André van Tonder
This SRFI is currently in ``final'' status. To see an explanation of each
status that a SRFI can hold, see
here.
It will remain in draft status until 2005/08/14, or as amended. To
provide input on this SRFI, please
mailto:srfi minus 72 at srfi dot schemers dot org
.
See instructions
here to subscribe to the list. You can access previous messages via
the
archive of the mailing list.
本 SRFI では以下の性質を持った Scheme 用の手続き型マクロについて述べる。
syntax-case
syntax-case
をより単純なプリミティブにより表現されるライブラリとして定義する。
car
、cdr
、cons
等々を使用することができる。
make-capturing-identifier
を提供する。
簡単な例から始める。
(define-syntax (swap! a b) (quasisyntax (let ((temp ,a)) (set! ,a ,b) (set! ,b temp))))
ここで、構文オブジェクトは quasisyntax
を使って作成される。入力に由来する構文は unquote
と unquote-splicing
をもちいて出力に挿入することができる。この方法で書かれたマクロは健全で参照透過である。
次の例では構文オブジェクトに対して、手続き car
、cdr
、null?
、...
を使用することができることを示す。またさらに入力式中のリテラルを識別するのに述語 literal-identifier=?
を使用する方法を示す。
(define-syntax (my-cond c . cs) (if (literal-identifier=? (car c) (syntax else)) (quasisyntax (begin ,@(cdr c))) (if (null? cs) (quasisyntax (if ,(car c) (begin ,@(cdr c)))) (quasisyntax (if ,(car c) (begin ,@(cdr c)) (my-cond ,@cs))))))
本提案では、syntax-case
はより単純なプリミティブにより表現されるライブラリとして定義されている。したがって my-cond
マクロは以下のように表現することができる。
(define-syntax my-cond (lambda (form) (syntax-case form (else) ((_ (else e1 ...) c1 ...) (syntax (begin e1 ...))) ((_ (e0 e1 ...)) (syntax (if e0 (begin e1 ...)))) ((_ (e0 e1 ...) c1 ...) (syntax (if e0 (begin e1 ...) (my-cond c1 ...)))))))
以下に示すように、従来の健全なマクロシステムでは、手続き型のマクロでは意図しない変数捕捉が起こることがある。
(let-syntax ((main (lambda (form) (define (make-swap x y) (quasisyntax (let ((t ,x)) (set! ,x ,y) (set! ,y t)))) (quasisyntax (let ((s 1) (t 2)) ,(make-swap (syntax s) (syntax t)) (list s t)))))) (main)) ==> (1 2) -- 従来の健全性アルゴリズム (2 1) -- 本 SRFI の提案手法
従来の健全性アルゴリズムでは、マクロ呼び出し全体で導入された同名の識別子はすべて同一のものとみなしているからである。
このために、プログラムの別々の箇所で固定的な意味を持ったまま使用されるコード断片を手続き的に構成することが難しくなっている。これは、コード断片の意味が使用箇所で意図せず損われることがあるからである。このことにより、参照透過性の考え方がそこなわれ、巨大なマクロや複数の補助手続きがマクロ群で共有される場合に脆弱性をもたらすことがある。
この問題は、以下の例のような、一見単純なマクロでも確認することができる。let-in-order
フォームは束縛が左から右に評価されることが保証された let
である。ここでも、従来型の健全性アルゴリズムでは、再帰的に挿入された t
のインスタンスが意図せず捕捉されることになってしまう。
(define-syntax let-in-order (lambda (form) (syntax-case form () ((_ ((i e) ...) e0 e1 ...) (let f ((ies (syntax ((i e) ...))) (its (syntax ()))) (syntax-case ies () (() (quasisyntax (let ,its e0 e1 ...))) (((i e) . ies) (quasisyntax (let ((t e)) ,(f (syntax ies) (quasisyntax ((i t) ,@its)))))))))))) (let-in-order ((x 1) (y 2)) (+ x y)) ==> 4 -- 従来の健全性アルゴリズム 3 -- 本 SRFI の提案手法
これらは、少しずつ異なったものに見えはするが、まさしく健全性で解決すべき種類の問題である。これらの問題を解決するために、本 SRFI では次の修正版健全性規則 [2-4] を提案する。
識別子の束縛は以下の場合にだけ別の識別子を捕捉する。すなわち、両者が入力に表れた場合、syntax ないし quasisyntax の単一の呼び出しで導入された場合である。ただしこのとき、unquote された syntax および quasisyntax は外側の quasisyntax に属するものと解釈される。
最初の例では、この規則により、t
のふたつのインスタンスは別のものと識別される。これはどちらも別の quasisyntax
に現れるからである。二番目の例では、t
は別々の quasisyntax
の呼び出しで導入されるものであるから、これも別のものと見做される。本提案により、上のマクロは意図通りの正しいものとなった。
Reflective tower は互いに独立な環境の列からなり、各環境は与えられた実行フェーズ中で有効になる束縛を決定する [11, 14]。
次の例では、二番目の定義と m
の束縛の右辺は展開時の環境で評価され、最初の定義と let-syntax
の展開結果の式は実行時の環境で評価される。このふたつの環境から二段階の Reflective tower が成る。
(define x 1) (begin-for-syntax (define x 2)) (let-syntax ((m (lambda (form) (quasisyntax (list x ,x))))) (m)) ==> (1 2)
(list x ,x)
の二番目の x
は展開時に評価され、一番目の x
は実行時に評価される。これらはそれぞれ適切な環境から探索される。
環境の tower の必要性は以下のように理解される。すなわち、インタプリタでは、展開時の束縛と実行時の束縛は同時メモリ空間上に存在する。これは、展開と評価を切り替えるためである。これらの束縛は、以下の、静的スコープ言語の基本的な重要性である特性を保証するために、厳格に区別されていなければならない。
プログラムの意味はその字面から明確に区別できなければならない。
これを理解するために、上の式をひとつひとつ展開し評価することを考える。環境を単一のものにし、二番目の定義が一番目の定義を隠蔽することを認めた場合、プリコンパイルしたものとは結果が異なってしまい、プログラムの意味が曖昧になってしまう。
フェーズ間での隠蔽がないとすると、Reflective Tower の存在は次のような静的スコープの規則で表現することができる。
識別子の意味は、その識別子の使用されるフェーズで字面上可視な束縛により決定される。
本 SRFI はこの規則をトップレベルと局所束縛との両方に適用する、同等のマクロシステムとは異なる。上の例を次のものと比べてみよう。
(let ((x 1)) (let-syntax ((m (lambda (form) (let ((x 2)) (quasisyntax (list x ,x)))))) (m))) ==> (1 2)
ここで、内側の let
は実行時の環境に束縛を定義する。先に述べたとおり、この束縛は外側の、実行時環境の束縛を隠蔽することはできない。(list x ,x)
中の x
はそれぞれ、評価時の適切な環境から探索される。
Reflective tower は任意の高さを持つことができる。以下の例では、実行フェーズと展開フェーズに加えて、内側のマクロの右辺はメタ展開時フェーズに評価されることになる。したがって、このとき Reflective tower には環境がみっつあり、また当然、任意の段階までこれを繰り返すことができる。begin-for-syntax
を入れ子にすると任意のフェーズの束縛を指定することができる。下の (list x ,x ,,x)
中の x
はそれぞれ別個のフェーズの変数として扱われるのである。
(define x 0) (begin-for-syntax (define x 1) (begin-for-syntax (define x 2))) (let-syntax ((foo (lambda (form) (let-syntax ((bar (lambda (form) (quasisyntax (quasisyntax (list x ,x ,,x)))))) (bar))))) (foo)) ==> (0 1 2)
(syntax ...)
中のテンプレートの省略記号は、省略記号そのものではなく、ただの識別子として解釈するものとしている。このため、次のようなイディオムを使うと syntax-case
で生成されたマクロに省略記号を含めることができる。
(let-syntax ((m (lambda (form) (syntax-case form () ((_ x ...) (with-syntax ((::: (syntax ...))) (syntax (let-syntax ((n (lambda (form) (syntax-case form () ((_ x ... :::) (syntax `(x ... :::))))))) (n a b c d))))))))) (m u v)) ==> (a b c d)
以下のものがプリミティブとして提供される。
以下のものはライブラリとして提供される。
構文オブジェクトは、Scheme のペアないしベクタをノードとし、定数ないし識別子を葉とするグラフである。以下の式は構文オブジェクトに評価される。
'() 1 #f '(1 2 3) (cons (syntax x) (vector 1 2 3 (syntax y))) (syntax (let ((x 1)) x)) (quasisyntax (let ((x 1)) ,(syntax x)))
シンボルは構文オブジェクト中にあらわれてはならない。
'(let ((x 1)) x) ==> 構文オブジェクトではない
Reflective tower は互いに独立な環境の列からなり、各環境により与えられた実行フェーズ中で有効になる束縛が決定する。マクロ定義中に let-syntax
を入れ子にすることで、フェーズ数、すなわち Reflective tower の高さを任意の段階に大きくすることができる。
tower の各段階の環境は、本 SRFI で述べるプリミティブと同様に、ホストとなる Scheme システムの標準的な束縛を初期状態として持っている。
識別子の意味は、その識別子の使われるフェーズにおいて字面上可視な束縛により決定される。
以下の例では、m
の右辺の (syntax x)
は展開時のオブジェクトに評価され、内側の束縛の影響を受けない。展開結果の識別子は展開されたコードでは実行時の変数として扱われ、外側の束縛を意味することになる。
(let ((x 1)) (let-syntax ((m (lambda (form) (let ((x 2)) (syntax x))))) (m))) ==> 1
次の例では、マクロ n
によって導入される x
のインスタンスはふたつある。ひとつは展開時に使われ、2
への束縛を参照し、ふたつめは実行時に使われ、1
への束縛を参照する。
(let ((x 1)) (let-syntax ((m (lambda (form) (let ((x 2)) (let-syntax ((n (lambda (form) (syntax (let ((y x)) (quasisyntax (list x ,y))))))) (n)))))) (m))) ==> (1 2)
下で説明するプリミティブ begin-for-syntax
は任意の reflective level での計算を指定する。
ブロック構造のプログラミング言語の基本原則は、プログラムの意味はその文字表現にのみ依存する、というものである。特に、実装系は次の原則について考慮しなければならない。
トップレベルのフォームを、インタプリタでひとつひとつ展開・評価した場合も、最初に全体を展開してコンパイルし評価した場合も、どちらの場合も同一の結果が得られなければならない。
この原則を以下に適用した場合、名前空間と字句スコープそれぞれを各 Reflective level ごとに区別して管理する必要があることを示す。インタプリタの場合、のふたつの x
の束縛は同時にメモリ中に存在する。もし環境がひとつに統合されてい、一方の束縛で他方が隠蔽されることを認めてしまうと、実行結果はプリコンパイルしたものを実行した場合とは異なったものになってしまうだろう。
(define x 1) (begin-for-syntax (define x 2)) (let-syntax ((m (lambda (form) (quasisyntax (list x ,x))))) (m)) ==> (1 2)
手続き型のマクロシステムでは、展開順序は観測可能である。展開順序は以下の最低限のことを除いて未定義としておく。これは潜在的な便利に応じて選べるようにするためである。山括弧中の用語の意味は R5RS の 7.1 節に定義されている。
<expression>
はすべてアトミックに展開される。
<body>
の最初の <expression>
の展開中でも同じである。
<sequence>
はすべて左から右へ評価される。
<program>
中の <command or definition>
はすべて左から右にアトミックに展開される。
exp
は現在のトップレベル構文環境で展開・評価される。評価結果は syntax-object -> syntax-object
型の手続きにならなければならない。これを変換子とも呼ぶ。トップレベルの構文環境は、識別子 keyword
を結果の変換子に束縛することで拡張される。
二番目の形式は以下と等価である。
(define-syntax keyword (let ((transformer (lambda (dummy . formals) exp1 exp ...))) (lambda (form) (apply transformer form))))
R5RS 4.3.1 節を一般化し、各式 exp
が任意の変換子手続きに評価されることを認める。
また、let[rec]-syntax
は新しいスコープを導入せずに、そこにフォームを挿入したかのように振る舞わなければならない。以下の例を参照。
(let ((x 1)) (let-syntax ((foo (syntax-rules ()))) (define x 2)) x) ==> 2
obj
が識別子であれば #t
を返し、さもなくは #f
を返す。識別子は R5RS の 3.2 節で述べられている、他の Scheme のプリミティブ型と互いに独立である。
識別子がマクロ展開の結果自由変数として挿入され、同一の静的束縛、ないしトップレベル束縛を参照している場合、それらは free-identifier=?
である。このとき、静的に束縛されていない識別子はすべて暗黙にトップレベルに束縛されているものとする。
識別子が free-identifier=?
であるとき、またはそれらがトップレベル束縛を参照し、記号として同一の名前を持つとき、それらは literal-identifier=?
である。この手続きは、(cond
文の else
のような)リテラルがそのマクロの定義とは異なるモジュールに現れた場合にも、確実にそれを識別できるように使用する。
識別子がふたつあり、一方の束縛がその束縛のスコープ内でもう一方の識別子への参照を捕捉する可能性があるとき、それらは bound-identifier=?
である。
同名の二識別子が、どちらももとのプログラムの同一のトップレベル式内に現れていた場合、それらは bound-identifier=?
である。また、既存の bound-identifier=?
である識別子から、単一の syntax
ないし quasisyntax
フォームの評価により生成されたもの同士も、bound-identifier=?
である。このとき、入れ子になって、unquote された syntax
ないし quasisyntax
フォームの評価は、その外側の quasisyntax
の評価の一部であると見做される。またさらに、datum->syntax-object
により、以前に挿入された識別子に bound-identifier=?
な識別子が作成されることもある。
これらの識別子は、引数が識別子でない場合にも #f
を返す。
(free-identifier=? (syntax x) (syntax x)) ==> #t (bound-identifier=? (syntax x) (syntax x)) ==> #f (let ((y (syntax (x . x)))) (bound-identifier=? (car y) (cdr y))) ==> #t (quasisyntax ,(bound-identifier=? (syntax x) (syntax x))) ==> #t (let ((x 1)) (let-syntax ((m (lambda (form) (quasisyntax (let ((x 2)) (let-syntax ((n (lambda (form) (free-identifier=? (cadr form) (syntax x))))) (n ,(cadr form)))))))) (m x))) ==> #f
datum
から構文オブジェクトを新規に作成する。この構文オブジェクトは次のようにして入力フォームに埋め込まれる。すなわち、datum
中の定数は変化せず、識別子は、既存のどの識別子とも bound-identifier=?
について異なる識別子であるような、生新無垢な識別子に置き換えられる。結果中の二識別子が bound-identifier=?
であるのは、datum
中のもともと bound-identifier=?
である識別子を、単一の syntax
フォームの評価中に置き換えた場合である。
これらの新しい識別子は依然としてもとの識別子とは free-identifier=?
である。これは、マクロ展開によりその識別子が束縛部分に現れない場合、新しい識別子は datum
中のもとの識別子と同じ束縛を意味するということである。
ここで述べた syntax
フォームにはパターン変数の挿入のための記法は存在しないが、syntax-case
のスコープ内ではそのような機能を持つものに事実上再束縛される(下記を参照)。
例:
(bound-identifier=? (syntax x) (syntax x)) ==> #f (let ((y (syntax (x . x)))) (bound-identifier=? (car y) (cdr y))) ==> #t (syntax-object->datum (syntax (x ...))) ==> (x ...) (define (generate-temporaries list) (map (lambda (ignore) (syntax temp)) list))
datum
中で bound-identifier=?
について区別されていた識別子は、それらが記号として同一の名前を持つ場合でも、syntax
の呼び出しによりそれらが同一のものとして扱われるようになることはないことに注意。
(let ((x 1)) (let-syntax ((foo (lambda (form) (quasisyntax (let-syntax ((bar (lambda (_) (syntax (let ((x 2)) ,(cadr form)))))) (bar)))))) (foo x))) ==> 1
template
から構文オブジェクトを新規に作成する。このとき、template
では unquote
や unquote-splicing
をつかって unquote することができる。一番外側の quasisyntax
に対応する unquote 部分式がない場合、(quasisyntax template)
の評価結果は (syntax template)
の評価結果と等価である。しかし、unquote 式が現れた場合には、それらは R5RS 4.2.6 の quasiquote
の項に説明された規則に従い、評価、挿入/接合される。
入れ子になった unquote-splicing
を便利にするために、さらに、文献 [10] の付録 B にある R5RS 互換の quasiquote
拡張に必要な変更を加えて quasisyntax
に追加する必要がある。
quasisyntax
の評価により導入された識別子は、既存のどの識別子とも、bound-identifier=?
について異なる。結果中のふたつの識別子が bound-identifier=?
であるのは、既存の bound-identifier=?
なる識別子を、単一の auasisyntax
呼び出しの template
中で置き換えた場合だけである。このとき、入れ子になり、unquote された syntax
ないし quasisyntax
の評価は、外側の quasisyntax
の評価の一部と見做される。
これらの新しい識別子は依然として、もとの識別子と free-identifier=?
である。これは、マクロ展開によりその識別子が束縛部分に現れない場合、新しい識別子は datum 中のもとの識別子と同じ束縛を意味するということである。
ここで述べた quasisyntax
フォームにはパターン変数の挿入のための記法は存在しないが、syntax-case
のスコープ内ではそのような機能を持つものに事実上再束縛される(下記を参照)。
例:
(bound-identifier=? (quasisyntax x) (quasisyntax x)) ==> #f (quasisyntax ,(bound-identifier=? (quasisyntax x) (syntax x))) ==> #t (let-syntax ((f (lambda (form) (syntax (syntax x))))) (quasisyntax ,(bound-identifier=? (f) (f)))) ==> #f (let-syntax ((m (lambda (_) (quasisyntax (let ((,(syntax x) 1)) ,(syntax x)))))) (m)) ==> 1
上の例で、quasisyntax
内の bound-identifier=?
の等価性規則から、次の等価性が導かれることに注意。
(quasisyntax (let ((,(syntax x) 1)) ,(syntax x))) <-> (quasisyntax (let ((x 1)) x))
入れ子になった quasisyntax
を含むような、伝統的なマクロ生成マクロのイディオムも正常に動作する。
(let-syntax ((m (lambda (form) (let ((x (cadr form))) (quasisyntax (let-syntax ((n (lambda (_) (quasisyntax (let ((,(syntax ,x) 4)) ,(syntax ,x)))))) (n))))))) (m z)) ==> 4
quasisyntax
内の bound-identifier=?
の等価性規則は、上のマクロが以下の syntax-case
マクロとまったく同等であることを保証する唯一のものである。
(let-syntax ((m (lambda (form) (syntax-case form () ((_ x) (syntax (let-syntax ((n (lambda (_) (syntax (let ((x 4)) x))))) (n)))))))) (m z)) ==> 4
obj
を以下のように構文オブジェクトに変換する(このとき、obj
はペアないしベクタをノードとし、シンボルないし定数を葉とするグラフでなければならない)。すなわち、obj
中の定数は変化せず、シンボルは、bound-identifier=?
、free-identifier=?
、literal-identifier=?
のもとで、記号として同一の名前を持つ識別子と同様にふるまい、かつ template-identifier
と同一のソースのトップレベル式と同時に出現したかのように、あるいは同一の syntax
または quasisyntax
の評価段階で生成されたかのようにふるまう識別子と置き換えられる。
template-identifier
が動的な識別子であった場合、obj
中のシンボルも動的な識別子に変換される。
(let-syntax ((m (lambda (_) (let ((x (syntax x))) (let ((x* (datum->syntax-object x 'x))) (quasisyntax (let ((,x 1)) ,x*))))))) (m)) ==> 1 (let ((x 1)) (let-syntax ((m (lambda (form) (quasisyntax (let ((x 2)) (let-syntax ((n (lambda (form) (datum->syntax-object (cadr form) 'x)))) (n ,(cadr form)))))))) (m z))) ==> 1
datum->syntax-object
により、健全かつ意図的に既存の参照を捕捉する束縛を挿入することができるようになる。このようなマクロの組み合わせは微妙な問題であるため、文献にも誤った例が多々現れている。ここには [2] に挙げられている周到かつ精密な例を示す。
(define-syntax if-it (lambda (x) (syntax-case x () ((k e1 e2 e3) (with-syntax ((it (datum->syntax-object (syntax k) 'it))) (syntax (let ((it e1)) (if it e2 e3)))))))) (define-syntax when-it (lambda (x) (syntax-case x () ((k e1 e2) (with-syntax ((it* (datum->syntax-object (syntax k) 'it))) (syntax (if-it e1 (let ((it* it)) e2) (if #f #f)))))))) (define-syntax my-or (lambda (x) (syntax-case x () ((k e1 e2) (syntax (if-it e1 it e2)))))) (if-it 2 it 3) ==> 2 (when-it 42 it) ==> 42 (my-or 2 3) ==> 2 (my-or #f it) ==> Error: undefined identifier: it (let ((it 1)) (if-it 42 it #f)) ==> 42 (let ((it 1)) (when-it 42 it)) ==> 42 (let ((it 1)) (my-or 42 it)) ==> 42 (let ((it 1)) (my-or #f it)) ==> 1 (let ((if-it 1)) (when-it 42 it)) ==> 42
my-or
では意図的に it
を利用者に公開していないことに注意。一方、when-it
の定義では明示的に it
を利用者に公開し直している。この場合も最後の例のように参照透過性は保たれている。
構文オブジェクト中の識別子をその名前をあらわすシンボルで置き換えたグラフを返す。
(datum->syntax-object template-identifier symbol)
と free-identifier=?
である生新無垢な識別子を返す。この識別子は既存のどの識別子とも bound-identifier=?
ではない。この識別子が束縛フォーム中の束縛変数として挿入されると、この識別子の束縛は同スコープ内の free-identifier=?
である識別子すべてを捕捉する。
このプリミティブは datum->syntax-object
の代わりに意図的な変数捕捉をするのに使うことができる。このとき、変数捕捉は bound-identifier=?
ではなく free-identifier=?
にもとづいておこなわれるため、その実装と意味論はいくらか異なってくる。以下について考えてみよう。
(define-syntax if-it (lambda (x) (syntax-case x () ((k e1 e2 e3) (with-syntax ((it (make-capturing-identifier (syntax here) 'it))) (syntax (let ((it e1)) (if it e2 e3)))))))) (define-syntax when-it (lambda (x) (syntax-case x () ((k e1 e2) (syntax (if-it e1 e2 (if #f #f))))))) (define-syntax my-or (lambda (x) (syntax-case x () ((k e1 e2) (syntax (let ((thunk (lambda () e2))) (if-it e1 it (thunk)))))))) (if-it 2 it 3) ==> 2 (when-it 42 it) ==> 42 (my-or 2 3) ==> 2 (my-or #f it) ==> undefined identifier: it (let ((it 1)) (if-it 42 it #f)) ==> 1 (let ((it 1)) (when-it 42 it)) ==> 1 (let ((it 1)) (my-or 42 it)) ==> 42 (let ((it 1)) (my-or #f it)) ==> 1 (let ((if-it 1)) (when-it 42 it)) ==> 42
when-it
が上の datum->syntax-object
を使った定義よりも単純になっていることに注意。これは、束縛を明示的に敷衍させなくてもよいからである。しかしその一方で、my-or
にあるように、束縛を制限するための作業も必要になる。また、ふたつの方法では評価結果も異なってい、ここでは明示的な束縛が暗黙の束縛よりも高い優先順位をもっている。これは [13] で提案されている MzScheme の方法と同じであるが、make-capturing-identifier
の第一引数を変えることで動作を変更することもできる。
このプリミティブを使うと展開時の動的束縛フォームを実装することができる。下の例に Chez Scheme の fluid-let-syntax
[6, 7] の実装の仕方を示す。
(define-syntax fluid-let-syntax (lambda (form) (syntax-case form () ((_ ((i e) ...) e1 e2 ...) (with-syntax (((fi ...) (map (lambda (i) (make-capturing-identifier i (syntax-object->datum i))) (syntax (i ...))))) (syntax (let-syntax ((fi e) ...) e1 e2 ...))))))) (let ((f (lambda (x) (+ x 1)))) (let-syntax ((g (syntax-rules () ((_ x) (f x))))) (let-syntax ((f (syntax-rules () ((_ x) x)))) (g 1)))) ==> 2 (let ((f (lambda (x) (+ x 1)))) (let-syntax ((g (syntax-rules () ((_ x) (f x))))) (fluid-let-syntax ((f (syntax-rules () ((_ x) x)))) (g 1)))) ==> 1
このフォームはトップレベルにだけ現れる。マクロ展開時に form ...
を現在展開の行われているのよりも一段階 reflective level の高い環境で左から右に評価する。戻り値は規定されていず、実行時にふたたび評価されることもない。
(define x 1) (begin-for-syntax (define x 2)) (let-syntax ((m (lambda (form) (quasisyntax (list x ,x))))) (m)) ==> (1 2)
begin-for-syntax
を入れ子にすると、任意のフェーズ、reflective tower の任意の段階にトップレベル束縛を導入し、計算を実行することができる。
(begin-for-syntax (define x 1) (begin-for-syntax (define x 2))) (let ((x 0)) (let-syntax ((foo (lambda (form) (let-syntax ((bar (lambda (form) (quasisyntax (quasisyntax (list x ,x ,,x)))))) (bar))))) (foo))) ==> (0 1 2)
around-syntax
が展開されると、まず、式 before-exp
が評価され、次に form
全体が展開され、最後に、式 after-exp
が評価される。展開結果は form
全体の展開結果になる。
before-exp
と after-exp
は現在展開のおこなわれている段階よりも一段階高い reflective level で実行される。
このプリミティブをつかうと、マクロ記述者は部分式の展開の制御に使う情報を管理できるようになる。例えば、syntax-case
のリファレンス実装では、around-syntax
をつかいパターン変数の環境を管理し、syntax
テンプレートの展開を制御している。
(begin-for-syntax (define env (list (syntax a)))) (let-syntax ((foo (lambda (form) (quasisyntax ',env)))) (list (around-syntax (set! env (cons (syntax b) env)) (foo) (set! env (cdr env))) (foo))) ==> ((b a) (a))
構文エラーを通知する。obj ...
を表示し、その時点のソース・オブジェクト間の関係を、表示もしくはデバッグツールに渡しなどして、展開器を停止する。
syntax-case
は上で述べたプリミティブを使ってマクロとして記述することができる。
パターン中の識別子で、(literal ...)
中の識別子のいずれとも bound-identifier=?
でないものはパターン変数と呼ばれる。これらは、通常の変数と同一の名前空間に置かれ、以降に現れる変数をあるいは隠蔽し、あるいは隠蔽される。各パターンは syntax-rule
のパターン(R5RS 4.3.2)と同一で、R5RS 4.3.2 の規則にしたがい入力式 exp
に対してマッチングを行う。(ただし、パターンの最初の部分は無視される)。このとき、exp
は構文オブジェクトに評価される。パターン中の識別子が (literal ...)
中の識別子と bound-identifier=?
である場合、それらの識別子は literal-identifier=?
をつかって入力中の識別子とマッチングがおこなわれる。パターンがマッチしても、フェンダー式が存在し、かつそれが #f
に評価された場合は、評価は次の節に移動する。
各節の fender
および output-expression
中の (syntax template)
と (quasisyntax template)
は再束縛され、pattern
中のパターン変数、および入れ子になった syntax-case
で可視のパターン変数が、template
中ではパターンに一致した入力の部分フォームで置き換えられるようになっている。このために、(syntax template)
中の template
は syntax-rules
の template
(R5RS 4.3.2)と同様に扱われるようになる。quasisyntax
は、部分テンプレートに unquote された式がない場合は syntax
と同等に扱われる。
生新無垢な識別子の bound-identifier=?
等価性規則により、テンプレート中の識別子で、パターン変数を参照しないものが置き換えられることは、上の syntax
および quasisyntax
の節で述べた通りである。
テンプレート中の省略記号で、識別子の先行しないものは省略記号リテラルとは解釈されない。このため、次のようなイディオムをつかって、省略記号を含むマクロを生成することができる。
(let-syntax ((m (lambda (form) (syntax-case form () ((_ x ...) (with-syntax ((::: (syntax ...))) (syntax (let-syntax ((n (lambda (form) (syntax-case form () ((_ x ... :::) (syntax `(x ... :::))))))) (n a b c d))))))))) (m u v)) ==> (a b c d)
[6, 7] にあるとおり、with-syntax
は以下のような syntax-case
に展開される。
(define-syntax with-syntax (lambda (x) (syntax-case x () ((_ ((p e0) ...) e1 e2 ...) (syntax (syntax-case (list e0 ...) () ((p ...) (begin e1 e2 ...))))))))
R5RS の 4.3.2 節を参照。[6, 7] にあるとおり、syntax-case
をつかって以下のように定義することができる。
(define-syntax syntax-rules (lambda (x) (syntax-case x () ((_ (i ...) ((keyword . pattern) template) ...) (syntax (lambda (x) (syntax-case x (i ...) ((dummy . pattern) (syntax template)) ...)))))))
Reader を拡張して #'e
が (syntax e)
、#`e
が (quasisyntax e)
となるようにすることが推奨される。
現在のところみっつの実装が利用可能である。参照実装、CHICKEN コンパイラ用の実装、それから PLT DrScheme に統合された、ソース位置追跡機能と syntax highlighting の搭載されたものである。
参照実装では R5RS で規定されたフォームと手続きを使っている。マクロは R5RS に規定されたものもほかの既存のものも必要としない。また、interaction-environment
と(たいていの実装で利用可能なものであるが)引き数なし版の eval
を使用している。
参照実装は http://www.het.brown.edu/people/andre/macros/index.htm にある。これは少なくとも Chez と CHICKEN と Gambit、MzScheme で実行できた。本実装は [8, 11] に述べられている明示的な改名システムに強い影響を受けている。そのシステムは shallow binding にもとづく命令型の高速な健全性アルゴリズムを用いてい、先行順に評価が進み、式のサイズに対して線型である。
本 SRFI の提案を CHICKEN Scheme コンパイラの言語拡張として実装したものは http://www.call-with-current-continuation.org/ にある。
PLT DrScheme に統合された版では、ソース・オフジェクト間の関係追跡機能を実装し、展開時および実行時のエラー用の syntax highlighting を提供している。これは http://www.het.brown.edu/people/andre/macros/index.htm にある。
本仕様では複合型構文オブジェクトは通常のリストやベクタで表現されるとしている。これはつまり、ソースの位置情報を構文オブジェクト自体に格納することができないということである。
このような表現をした場合、ソース情報の追跡は Dybig と Hieb による手法を使っておこなわれる [2]。展開器は単純に各リストおよび各識別子(の各出現)のソース情報を何らかの外部データ構造(例えば、ハッシュテーブル)に記録し管理する。そのため、各識別子の出現を区別するために、余計なラッパーが必要になるだろう。
Special thanks to Kent Dybvig, Matthew Flatt and Felix Winkelmann for helpful comments.
tower: a tall piece of furniture used for storing things.
Macros are reflective tools that operate on the representation of programs.
unquote
, unquote-splicing
Copyright (C) André van Tonder (2005). All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.