For Gauche 0.9.6


Next: , Previous: , Up: マクロ   [Contents][Index]

5.2 衛生的マクロ

マクロ束縛

以下のフォームはtransformer-specで作られるマクロ変換器と nameの束縛を作ります。外側のスコープにnameの束縛があれば、 それはシャドウされます。

トップレベル束縛の場合、nameに他のモジュールからインポートされたり 継承されている束縛があれば、それをシャドウすることになります (モジュール参照)。 (註:モジュール内でのトップレベル束縛がインポートした束縛をシャドウするのは Gaucheの拡張です。R7RSではインポートした束縛の再定義はしてはいけないことに なっているので、ポータブルなコードでは避けて下さい)。

同じスコープで同じ名前を複数回束縛した場合の動作は未定義です。

transformer-specsyntax-rulesフォーム、 er-macro-transformerフォーム、あるいは他のマクロキーワードか 構文キーワードです。これについては後述します。

Special Form: define-syntax name transformer-spec

[R7RS base] トップレベルで使われた場合、このフォームはトップレベルのnametransformer-specで定義されるマクロ変換器に束縛します。

lambdalet等の本体の宣言部分に使われた場合 (内部define-syntax)、 その本体内のスコープでnameを束縛します。 内部defineがletrec*に変換されるのと同じように、 内部define-syntaxはletrec-syntaxへと変換されます。

Special Form: let-syntax ((name transformer-spec) …) body
Special Form: letrec-syntax ((name transformer-spec) …) body

[R7RS base] ローカルマクロを定義します。各nameが 対応するtransformer-specで定義されるマクロ変換器へと束縛された 環境を作りbodyを評価します。 let-syntaでは、transformer-speclet-syntaxを 囲むスコープ内でtransformer-specを評価するのに対し、 letrec-syntaxではnameの束縛がなされた環境で transformer-specを評価します。つまりletrec-syntaxは 相互再帰的なマクロを定義できます。

Transformer specs

transformer-specは、マクロ展開器へと評価される特別な式です。 マクロ変換器はコンパイル時に実行されるため、他の式とは異なった段階で評価されます。 そのためにいくらか制限があります。

現在のところ、以下に挙げる式しか許されていません。

  1. syntax-rulesフォーム。これは「高レベル」マクロと呼ばれ、 パターンマッチングのみによってマクロを定義します。 これはSchemeとは異なる一種の宣言的言語で、 マクロの段階や衛生の問題をボンネットの下に隠してしまいます。 ある種のマクロはsyntax-rulesでより簡単に書けます。 詳しくはSyntax-rules macro transformerを参照してください。
  2. er-macro-transfomerフォーム。 これはexplicit renaming(ER)マクロを定義します。 ERマクロでは、必要な衛生を保ちながら、任意のSchemeコードを使って変換を書けます。 伝統的なLispのマクロは、ERマクロでリネームを使わない特別な場合と考えられます。 詳しくはExplcit-renamingマクロ変換器を参照してください。
  3. マクロキーワードか構文キーワード。これはGauche独自の拡張で、 既存のマクロキーワードや構文キーワードの別名を定義するものです。
    (define-syntax si if)
    (define écrivez write)
    
    (si (< 2 3) (écrivez "oui"))
    

Next: , Previous: , Up: 衛生的マクロ   [Contents][Index]

5.2.1 Syntax-rules macro transformer

Special Form: syntax-rules (literal …) clause clause2 …
Special Form: syntax-rules ellipsis (literal …) clause clause2 …

[R7RS base] パターンマッチングによるマクロ変換器を作ります。

clauseは次の形式です。

(pattern template)

patternはマクロ呼び出しにマッチすべきパターンを記述します。 パターンはS式で、マクロ呼び出しの式と同じ構造を持っている場合にマッチします。 但し、パターン中のシンボルはパターン変数と呼ばれ、 マクロ呼び出し式の対応する任意の部分木とマッチし、 templateの中でマッチした部分木を参照するのに使えます。

例えば、パターンが(_ "foo" (a b))であったとすると、それは (x "foo" (1 2))(x "foo" (1 (2 3)))といったマクロ呼び出しとマッチしますが、 (x "bar" (1 2))(x "foo" (1))(x "foo" (1 2) 3)とは マッチしません。 さらに、後で説明するように、繰り返しのある構造やリテラルシンボルとマッチするような記述も可能です。

clauseは順番に、そのパターンにマクロ呼び出しとマッチするかが検査されます。 マッチするパターンが見つかれば、対応するtemplateでマクロ呼び出しの式が 置き換えられます。template中のパターン変数は、 マクロ呼び出し式のその変数にマッチした部分木で置き換えられます。

これはなぜ衛生的マクロかで例に出したwhenマクロを syntax-rulesで書いたものです:

(define-syntax when
  (syntax-rules ()
    [(_ test body ...) (if test (begin body ...))]))

パターンが(_ test body ...)で、 テンプレートが(if test (begin body ...))です。 ... (エリプシス) は、記述を省略しているわけではなく、 ピリオド3つからなる名前を持つシンボルです。 これは直前のパターン(body)がゼロ個以上繰り替えされるということを示します。

whenマクロが (when (zero? x) (print "huh?") (print "we got zero!")) という形で呼び出されたとしまそう。 マクロ展開器はまず、この入力がパターンとマッチするかどうかを調べます。

bodyとのマッチングはちょっとややこしいです。 パターン変数bodyは配列のようなものだと考えても良いでしょう。 配列の各要素がマッチする入力の部分木を保持します。 その値は、テンプレート中の似たような繰り返し部分構造の中で使うことができます。 ここまでで入力がパターンにマッチしたので、テンプレートの方を見てみましょう。

エリプシスを使った展開はかなり強力です。 テンプレート中で、エリプシスは複数の値を持つパターン変数の直後にある必要はありません。 そういった変数を中に含む部分構造をエリプシスで繰り返すことも可能です。 次の例を見てください。

(define show
  (syntax-rules ()
    [(_ expr ...)
     (begin 
       (begin (write 'expr) (display "=") (write expr) (newline)) 
       ...)]))

このマクロを次のように呼ぶと:

(show (+ 1 2) (/ 3 4))

以下のとおりに展開されます (シンボルの衛生性は保たれているとします)。

(begin
  (begin (write '(+ 1 2)) (display "=") (write (+ 1 2)) (newline))
  (begin (write '(/ 3 4)) (display "=") (write (/ 3 4)) (newline)))

これを実行すれば、以下の出力が得られるでしょう。

(+ 1 2)=3
(/ 3 4)=3/4

また、パターン中の部分構造を繰り返しマッチするのにも使えます。 次の例はletlambdaに展開する、簡略化した例です:

(define-syntax my-let
  (syntax-rules ()
    [(_ ((var init) ...) body ...)
     ((lambda (var ...) body ...) init ...)]))

このマクロを(my-let ((a expr1) (b expr2)) foo)のように呼び出すと、 varaおよびbに、 initexpr1およびexpr2にそれぞれマッチします。 varinitはテンプレート中でばらばらに使うことができます。

パターン変数の繰り返しを示すエリプシスの入れ子の数を、 そのパターン変数のレベルと呼ぶことにします。 サブテンプレートは、その中に含まれるパターン変数のレベルの最大値と同じだけの エリプシスの入れ子に中になければなりません。 次の例では、パターン変数aのレベルは1 (最後のエリプシスによって繰り返される)、 bは2 (後ろの2つのエリプシスで繰り返される)、 cは3 (全てのエリプシスで繰り返される) です。

(define-syntax ellipsis-test
  (syntax-rules ()
    [(_ (a (b c ...) ...) ...)
     '((a ...)
       (((a b) ...) ...)
       ((((a b c) ...) ...) ...))]))

したがって、サブテンプレート a は1重、 サブテンプレート (a b)は2重、 (a b c)は3重のエリプシスで繰り返されることになります。

(ellipsis-test (1 (2 3 4) (5 6)) (7 (8 9 10 11)))
 ⇒ ((1 7)
    (((1 2) (1 5)) ((7 8)))
    ((((1 2 3) (1 2 4)) ((1 5 6))) (((7 8 9) (7 8 10) (7 8 11)))))

また、サブテンプレートの後ろには複数のエリプシスを直接置くことができ、 繰り返しの「葉」の部分がそこにスプライスされます。

(define-syntax my-append
  (syntax-rules ()
    [(_ (a ...) ...)
     '(a ... ...)]))

(my-append (1 2 3) (4) (5 6))
  ⇒ (1 2 3 4 5 6)

(define-syntax my-append2
  (syntax-rules ()
    [(_ ((a ...) ...) ...)
     '(a ... ... ...)]))

(my-append2 ((1 2) (3 4)) ((5) (6 7 8)))
  ⇒ (1 2 3 4 5 6 7 8)

註:サブテンプレートの直後に複数のエリプシスを置くこと、 及びパターン変数をそのレベルよりもエリプシスのネストが深いテンプレート中に置けることは、 R7RSに対する拡張で、SRFI-149で定義されています。上記の例では、 ellipsis-testmy-appendmy-append2が R7RSの範囲外になります。

パターン中に現れる識別子はパターン変数として扱われますが、 特定の識別子そのものにマッチさせたい場合もあります。例えば組み込みのcondcaseは、elseという識別子を特別に認識します。 literal …がその目的に使えます。次の例を見てください。

(define-syntax if+
  (syntax-rules (then else)
    [(_ test then expr1 else expr2) (if test expr1 expr2)]))

リテラルとして列挙された識別子はパターン変数にはならず、入力の識別子とそのままマッチします。 もし入力の該当する位置に同じ識別子が置かれていなければ、マッチは失敗します。

(if+ (even? x) then (/ x 2) else (/ (+ x 1) 2))
 expands into (if (even? x) (/ x 2) (/ (+ x 1) 2))

(if+ (even? x) foo (/ x 2) bar (/ (+ x 1) 2))
 ⇒ ERROR: malformed if+

これまで、シンボルと呼ばずに識別子という言葉を使っていました。大まかに言うと、 識別子とはシンボルに周囲の構文的環境をくっつけたもので、 衛生的マクロによるリネームによっても同一性を失わないようになっています。

次の例は失敗します。if+に渡されているelseletでローカルに 束縛されており、それはif+が定義された時点でのグローバルなelseとは 違うもので、したがってマッチしないからです。

(let ((else #f))
  (if+ (even? x) then (/ x 2) else (/ (+ x 1) 2))
  ⇒ ERROR: malformed if+

Previous: , Up: 衛生的マクロ   [Contents][Index]

5.2.2 Explcit-renamingマクロ変換器

Special Form: er-macro-transformer procedure-expr

procedure-exprからマクロ変換器を作ります。 作られたマクロ変換器は、define-syntaxlet-syntaxletrec-syntaxにより構文キーワードに束縛されなければなりません。 マクロ変換器の他の用途は定義されていません。

procedure-exprは3つの引数、formrenameid=?を 取る手続きへと評価される式です。

form引数には、マクロ呼び出しのS式そのものが渡されます。 procedure-exprはマクロ展開の結果をS式として返します。 この点は、伝統的なマクロとよく似ています。実のところ、 renameid=?を無視すれば、セマンティクスは伝統的な(非衛生な)マクロと 同じになります。次の例を見てください (この例ではmatchを使っています。マクロの入力を分解するのにも 手軽なツールです。)

(use util.match)

;; Unhygienic 'when-not' macro
(define-syntax when-not
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test expr1 expr ...)
         `(if (not ,test) (begin ,expr1 ,@expr))]
        [_ (error "malformed when-not:" form)]))))

(macroexpand '(when-not (foo) (print "a") 'boo))
  ⇒ (if (not (foo)) (begin (print "a") 'boo))

衛生を気にする必要がない場合は、これでも十分です。 例えばマクロを自分で書いたコードの中だけで使い、 すべてのマクロ呼び出しを把握していて名前の衝突が起きないことを知っている場合です。 けれども、このwhen-notマクロを広く使えるようにするなら、 マクロの使われる場所での名前の衝突からの防御が必要です。 たとえば、次のとおり呼び出されたとしてもちゃんと動くようにしたい場合です。

(let ((not values))
  (when-not #t (print "This shouldn't be printed")))

procedure-exprに渡されるrename引数は、 シンボル(正確には、シンボルか識別子)を取り、それをマクロ定義時の環境を保持する ユニークな識別子へと実質的にリネームする手続きです。 リネームされた識別子はマクロ使用時の環境には影響を受けません。

大雑把なルールとして、マクロの出力に挿入する識別子はすべてrenameを通すことを 徹底すれば、衛生は保たれます。when-notマクロの例では、 マクロの出力にifnotbeginを挿入していますから、 衛生的なバージョンは次のとおり書けます。

(define-syntax when-not
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test expr1 expr ...)
         `(,(rename 'if) (,(rename 'not) ,test)
            (,(rename 'begin) ,expr1 ,@expr))]
        [_ (error "malformed when-not:" form)]))))

でもこれは面倒ですし読みづらいですね。そこでGaucheでは、 補助マクロquasirenameを用意しています。これはquasiquoteのように 動作しますが、フォーム中の識別子をリネームしてゆきます。詳しくは後述の quasirenameのエントリを参照してください。quasirenameを使うと 衛生的なwhen-notはこうなります:

(define-syntax when-not
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test expr1 expr ...)
         (quasirename rename
           (if (not ,test) (begin ,expr1 ,@expr)))]
        [_ (error "malformed when-not:" form)]))))

シンボルをリネームせずに挿入すれば、意図的に衛生を破ることができます。 次のコードはアナフォリック(前方照応的)なwhenを定義しています。 つまり、テスト式の結果が、expr1 exprs … からitという 変数で参照できるということです。 itの束縛はマクロ呼び出し箇所には無かったもので、 マクロ展開器により挿入されるので、これは非衛生マクロになります。

(define-syntax awhen
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test expr1 expr ...)
         `(,(rename 'let1) it ,test     ; 'it' is not renamed
             (,(rename 'begin) ,expr1 ,@expr))]))))

quasirenameを使う場合、itがリネームされないようにするには ,'itと書きます。

(define-syntax awhen
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test expr1 expr ...)
         (quasirename rename
           (let1 ,'it ,test
             (begin ,expr1 ,@expr)))]))))

使用例を見てみましょう。

(awhen (find odd? '(0 2 8 7 4))
  (print "Found odd number:" it))
 ⇒ prints Found odd number:7

最後に、procedure-exprid=?引数はふたつの引数を取り、 それらがともに識別子であって、しかも同じ束縛を参照するか束縛されていないか、という 場合に限り#tを返します。 これはリテラル構文キーワード(condcaseフォームのelse等) を比較するのに使えます。

下のif=>マクロはifと同じように動作しますが、 (if=> test => procedure)のように呼ばれた場合、 (cond [test => procedure])構文と同じように、 testが真の値を返した際には結果を引数にしてprocedureを呼び出します。 シンボル=>は衛生的に比較されます。つまり、マクロ定義時と同じ束縛を 参照している場合にのみ有効となります。

(define-syntax if=>
  (er-macro-transformer
    (^[form rename id=?]
      (match form
        [(_ test a b)
         (if (id=? (rename '=>) a)
           (quasirename rename
             (let ((t ,test))
               (if t (,b t))))
           (quasirename rename
             (if ,test ,a ,b)))]))))

(rename '=>)とすることで、マクロ定義時における=>の束縛を 参照する識別子を手に入れ、id=?でそれをマクロ引数から渡された式と 比較しています。

(if=> 3 => list)  ⇒ (3)
(if=> #f => list) ⇒ #<undef>

;; 第二引数が=>でなければ、if=>は通常のifと同じ:
(if=> #t 1 2)     ⇒ 1

;; 下の例ではマクロ使用時の=>の束縛がマクロ定義時の束縛と違っているため、
;; => はリテラルと認識されず、if=> は通常のifとして振る舞う。
(let ((=> 'oof)) (if=> 3 => list)) ⇒ oof
Macro: quasirename renamer form

form中の「リテラル」な部分 (unquoteunquote-splicingの外側) に現れるシンボルや識別子がrenameによってリネームされることを除いて、 準クオートのように動作します。

例えば次のフォームは:

(quasirename r (a ,b c "d"))

次のとおり書くのと同じです:

(list (r 'a) b (r 'c) "d")

この手続きはマクロ専用というわけではありません。 renamerはシンボルか識別子を取る手続きであれば何でも構いません。

(quasirename (^[x] (symbol-append 'x: x)) (+ a ,(+ 1 2) 5))
  ⇒ (x:+ x:a 3 5)

ただ、ERマクロを書く際にとても便利なのは確かです。次の2つを比べてみてください。

(use util.match)

;; using quasirename
(define-syntax swap
  (er-macro-transformer
    (^[f r c]
      (match f
        [(_ a b) (quasirename r
                   (let ((tmp ,a))
                     (set! ,a ,b)
                     (set! ,b tmp)))]))))

;; not using quasirename
(define-syntax swap
  (er-macro-transformer
    (^[f r c]
      (match f
        [(_ a b) `((r'let) (((r'tmp) ,a))
                     ((r'set!) ,a ,b)
                     ((r'set!) ,b (r'tmp)))]))))

Previous: , Up: 衛生的マクロ   [Contents][Index]