Title

Hygienic macros.

Author

André van Tonder

Status

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 用の手続き型マクロについて述べる。

健全性の向上
従来の健全性アルゴリズムでは手続き型マクロでは意図しない変数捕捉が起こってしまうことについて議論し、これらの問題の起こらないアルゴリズムを提案する。
Reflective Tower
任意の高さの Refelective Tower を定義し、識別子の意味を決定する際に識別子の使われるフェーズを考慮するよう静的スコープの規則を改良することを提案する。
syntax-case
本提案では、syntax-case をより単純なプリミティブにより表現されるライブラリとして定義する。
手続き型インタフェース
複合型の構文オブジェクトを操作するインタフェースに特殊形式ではなく手続きをもちいる。特に、構文データに対して伝統的な carcdrcons 等々を使用することができる。
高速な健全性アルゴリズム
リファレンス実装では、正格で式のサイズに対して線型で高速な健全性アルゴリズムを説明する。
識別子の捕捉
意図的な変数捕捉と、展開時の動的束縛の構成ために make-capturing-identifier を提供する。

はじめに

簡単な例から始める。

  (define-syntax (swap! a b)
     (quasisyntax
       (let ((temp ,a)) 
         (set! ,a ,b) 
         (set! ,b temp))))

ここで、構文オブジェクトは quasisyntax を使って作成される。入力に由来する構文は unquoteunquote-splicing をもちいて出力に挿入することができる。この方法で書かれたマクロは健全で参照透過である。

次の例では構文オブジェクトに対して、手続き carcdrnull?... を使用することができることを示す。またさらに入力式中のリテラルを識別するのに述語 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

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

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 節に定義されている。

(define-syntax keyword exp)
(define-syntax (keyword . formls) exp1 exp ...)

exp は現在のトップレベル構文環境で展開・評価される。評価結果は syntax-object -> syntax-object 型の手続きにならなければならない。これを変換子とも呼ぶ。トップレベルの構文環境は、識別子 keyword を結果の変換子に束縛することで拡張される。

二番目の形式は以下と等価である。

  (define-syntax keyword 
     (let ((transformer (lambda (dummy . formals) exp1 exp ...)))
       (lambda (form)
         (apply transformer form))))
(let-syntax ((keyword exp) ...) exp* ...)
(letrec-syntax ((keyword exp) ...) exp* ...)

R5RS 4.3.1 節を一般化し、各式 exp が任意の変換子手続きに評価されることを認める。

また、let[rec]-syntax は新しいスコープを導入せずに、そこにフォームを挿入したかのように振る舞わなければならない。以下の例を参照。

 (let ((x 1))
    (let-syntax ((foo (syntax-rules ())))
      (define x 2))
    x)               ==> 2
(identifier obj)

obj が識別子であれば #t を返し、さもなくは #f を返す。識別子は R5RS の 3.2 節で述べられている、他の Scheme のプリミティブ型と互いに独立である。

(bound-identifier=? obj1 obj2)
(free-identifier=? obj1 obj2)
(literal-identifier=? obj1 obj2)

識別子がマクロ展開の結果自由変数として挿入され、同一の静的束縛、ないしトップレベル束縛を参照している場合、それらは 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
(syntax datum)

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
(quasisyntax template)

template から構文オブジェクトを新規に作成する。このとき、template では unquoteunquote-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
(datum->syntax-object template-identifier obj)

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 を利用者に公開し直している。この場合も最後の例のように参照透過性は保たれている。

(syntax-object->datum syntax-object)

構文オブジェクト中の識別子をその名前をあらわすシンボルで置き換えたグラフを返す。

(make-capturing-identifier template-identifier symbol)

(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
(begin-for-syntax form ...)

このフォームはトップレベルにだけ現れる。マクロ展開時に 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)

around-syntax が展開されると、まず、式 before-exp が評価され、次に form 全体が展開され、最後に、式 after-exp が評価される。展開結果は form 全体の展開結果になる。

before-expafter-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))
(syntax-error obj ...)

構文エラーを通知する。obj ... を表示し、その時点のソース・オブジェクト間の関係を、表示もしくはデバッグツールに渡しなどして、展開器を停止する。

(syntax-case exp (literal ...) clause ...)
clause := (pattern output-expression) | (pattern fender output-expression)

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) 中の templatesyntax-rulestemplate(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)
(with-syntax template)

[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 ...)))))))) 
(syntax-rules template)

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 の拡張

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]。展開器は単純に各リストおよび各識別子(の各出現)のソース情報を何らかの外部データ構造(例えば、ハッシュテーブル)に記録し管理する。そのため、各識別子の出現を区別するために、余計なラッパーが必要になるだろう。

Acknowledgments

Special thanks to Kent Dybvig, Matthew Flatt and Felix Winkelmann for helpful comments.

References

  1. André van Tonder: Portable macros and modules, http://www.het.brown.edu/people/andre/macros/index.htm
  2. R. Kent Dybvig: Private communication.
  3. Marcin 'Qrczak' Kowalczyk: Message on comp.lang.scheme, http://groups-beta.google.com/group/comp.lang.scheme/msg/b7075e4ca751dbdb
  4. Ben Rudiak-Gould: Message on comp.lang.scheme, http://groups-beta.google.com/group/comp.lang.scheme/msg/180c7627853c288e
  5. Matthew Flatt: Composable and Compilable Macros You Want it When?
  6. R. Kent Dybvig: Chez Scheme user's guide, http://www.scheme.com/csug/
  7. Robert Hieb, R. Kent Dybvig and Carl Bruggeman: Syntactic Abstraction in Scheme., R. Kent Dybvig: Writing hygienic macros in syntax-case http://library.readscheme.org/page3.html
  8. William D. Clinger: Hygienic macros through explicit renaming., http://library.readscheme.org/page3.html
  9. Eugene E. Kohlbecker, Daniel P. Friedman, Matthias Felleisen and Bruce F. Duba: Hygienic macro expansion, http://library.readscheme.org/page3.html
  10. Alan Bawden: Quasiquotation in Lisp, http://citeseer.ist.psu.edu/bawden99quasiquotation.html
  11. Richard Kelsey and Jonathan Rees: The Scheme 48 implementation, http://s48.org/
  12. Robert Hieb, R. Kent Dybvig: A compatible low-level macro facility, Revised(4) Report on the Algorithmic Language Scheme (appendix)
  13. Matthew Flatt: Introducing an Identifier into the Lexical Context of a Macro Call, http://list.cs.brown.edu/pipermail/plt-scheme/2004-October/006891.html
  14. Christian Queinnec: Macroexpansion Reflective Tower, http://www2.parc.com/csl/groups/sda/projects/reflection96/abstracts/queinnec.html

付: 訳語対照表

ellipsis
省略記号
fluid identifier
動的な識別子
fresh identifier
生新無垢な識別子、新しい識別子
hygiene algorithm
健全性アルゴリズム
phase
フェーズ
reflective level
reflective level
reflective tower
reflective tower。 Cf. reflection: 自己反映計算。 tower: a tall piece of furniture used for storing things. Macros are reflective tools that operate on the representation of programs.
unquote
unquote ← unquote, unquote-splicing

Copyright

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.


Author: André van Tonder
Editor: Francisco Solsona