For Development HEAD DRAFTSearch (procedure/syntax/module):

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-syntaxはまとめられて letrec-syntaxのように振る舞います。ただし、define-syntaxが出てきたところで 新たなスコープが作られるわけではありません。 例えば内部defineと内部define-syntaxを一つのスコープ内で混ぜこせに 並べることができます。重要なのは、内部define-syntaxで定義されるローカルマクロは、 その定義前にマクロ展開に必要とされてはならない、ということです。

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

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

Transformer specs

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

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

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

5.2.1 Syntax-rulesマクロ変換器

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!")) という形で呼び出されたとしましょう。 マクロ展開器はまず、この入力がパターンとマッチするかどうかを調べます。

  • パターン中のtestは入力の(zero? x)とマッチ。
  • パターン中のbodyは入力の(print "huh?")および(print "we got zero!")とマッチ

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

  • テンプレート中のifbeginはパターン中に現れていないので パターン変数ではありません。従って、識別子として出力に挿入されます。 ここで、識別子ifbeginはこのマクロのスコープから見えるグローバルな ifbeginを常に参照できるように、衛生的に扱われます。 マクロが使われた場所でifbeginがシャドウされていたとしても影響を受けません。
  • テンプレート中のtestはパターン変数なので、マッチした値である(zero? x)へと 置き換えられます。
  • bodyもパターン変数です。重要な点はここでもbodyの後にエリプシスがあることで、 bodyはパターン変数にマッチした値のぶんだけ繰り返されます。 最初のマッチした値である(print "huh?")と、次の (print "we got zero!")とがここに展開されます。
  • 以上から、最終的な展開結果として (if (zero? x) (begin (print "huh?") (print "we got zero!"))) が得られます (このうち、ifbeginは マクロ定義環境から見える識別子を指すようになっています)。

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

(define-syntax 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+

5.2.2 Explicit-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 quasiquoted-form

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

quasiquote-form引数は、準クオートされたフォームです。 最も外側の準クオート` 自体はquasirenameによって消費され、 出力には現れません。 この形にしたのは、ネストしたquasiquote/quasirenameを 正しく扱うためです。

例えば次のフォームは:

(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)))]))))

註: Gauche 0.9.7とそれ以前には、quasirenameは第二引数に 準クオートを必要としませんでした。つまり(quasirename r `form) と書くかわりに(quasirename r form)で良かったのです。

互換性のため、しばらくは準クオート無しのフォームも受け付けられます。

準クオートされたフォームを返すことを意図していた既存のコードについては、 準クオートを(quasirename r ``form)のように重ねる必要があります。

移行を容易にするため、quasirename中の準クオートの扱いを、 環境変数GAUCHE_QUASIRENAME_MODEでカスタマイズできます。 次に挙げる値を設定することができます。

legacy

quasirenameは、0.9.7やそれ以前と全く同様に動きます。 0.9.7用のコードを変更なしに動かす必要がある場合に使ってください。

compatible

quasirenameはこのエントリで説明した通りに動きます。 formが準クオートされていない場合、準クオートされているものと見なします。 準クオートされたフォームを作り出すことを意図している稀なケース以外の 既存のコードはこれで動かせるでしょう。

warn

quasirenameはこのエントリで説明した通りに動きますが、 formが準クオートされていなければ警告を出します。

strict

quasirenameformが準クオートされていることを要求し、 そうでなければエラーを投げます。将来はこの動作がデフォルトになる予定です。


5.2.3 識別子マクロ変換器

Special Form: make-id-transformer transformer-spec

transformer-specから識別子マクロ変換器を作ります。 transformer-specdefine-syntax等で使えるものと同じです。

通常のマクロは、Mをマクロが束縛された識別子とすると、(M arg …)という 形式でマクロ変換器を呼び出します。一方識別子マクロは、マクロに束縛された識別子単独、 もしくは(set! M expr)という形式でマクロ変換器を呼び出すものです。 言い換えれば、識別子マクロは、関数呼び出し形式ではなく変数参照形式で使われるものです。

次のコードを考えます。state-managerは状態を持つクロージャで、 識別子マクロthe-stateはクロージャを隠して あたかも変数にアクセスしているかのように見せます。

(define state-manager
  (let ([state #f])
    (case-lambda
      [() state]
      [(val) (set! state val)])))

(define-syntax the-state
  (make-id-transformer
    (syntax-rules (set!)
      [(set! _ expr) (state-manager expr)]
      [_ (state-manager)])))

(state-manager 'off)

the-state ⇒ off

(set! the-state 'on)

the-state ⇒ on

(state-manager) ⇒ on

(上のsyntax-rulesの二番目の節のパターンは_のみから なりますが、これはGaucheの拡張で、任意のフォームにマッチします。 したがってset!のマッチより後に置く必要があります)。

識別子マクロを使うとアクロバティックなことができますが、 すぐに読者を混乱させるコードになってしまいます。 一般的に、どうしても識別子マクロでなければ実現できないこと以外には使用を避けることをおすすめします。 ほとんどの場合、カッコを足して通常のマクロとして使うのが良いでしょう。

移植性が気になる場合は、util.identifier-syntaxモジュールの identifier-syntaxが使えるかもしれません (util.identifier-syntax - R6RS識別子マクロ参照)。 これはR6RSで規定されているので、サポートしている処理系も多いでしょう。 Gaucheではmake-id-transformerを使って util.identifier-syntaxを実装しています。



For Development HEAD DRAFTSearch (procedure/syntax/module):
DRAFT