(はじめよう Scheme 7)

マクロ


多くのLisp語族にはマクロと呼ばれるユーザ定義の構文を定義する機構が備わっている。Schemeも例外ではない。Schemeのマクロはhygienic macroと呼ばれるマクロである。これは健全なマクロもしくは清潔なマクロと訳されることが多いが、ここでは健全なマクロを訳語として使うこととする。小難しい用語が出てきたが、特に覚える必要はないので心配は不要である。

まずは簡単なマクロの例を見てみよう。
(define-syntax when
  (syntax-rules ()
    ((_ test expr ...)
     (if test (begin expr ...)))))
define-syntaxがマクロ定義の構文である。R7RSではdefine-syntaxは以下のように定義されている。
  • (define-syntax keyword transformer-spec)
transformer-specはR7RS-smallではsyntax-rulesのみが定義されている。syntax-rulesは与えられた式をパターンマッチで分解し、テンプレートで定義された式に展開する。syntax-rulesは以下のように定義されている。
  • (syntax-rules (identifier*syntax-rule)
  • (syntax-rules identifier (identifier*syntax-rule)
syntax-rule
  • (pattern template)
patternの部分でパターンマッチの定義を、templateの部分で展開式の定義をする。

ここで一つ実際にどのようにsyntax-rulesで定義された構文が展開されるのかを見てみよう。上記のwhenを使い、入力式として以下を考える:
(when (assq 'a lst) (display 'ok) (newline))
whenのパターンは(_ test expr ...)である。R7RSでは_(アンダースコア)は何にでもマッチするプレースホルダーとして定義されているので、この場合は最初のwhenがマッチする。パターンの一つ目がマッチしたので、二つ目を見る。testのような識別子はリストの要素一つにマッチするので、(assq 'a lst)がマッチする。最後expr
は多少特殊なマッチをする。exprの後ろに...(三点、ellipsisと呼ばれる)がある場合は0個以上の要素にマッチするという定義になる。この場合は(display 'ok)(newline)の二つにマッチする。
(when (assq 'a lst) (display 'ok) (newline))
#|
マッチ結果
_        -> when
test     -> (assq 'a lst)
expr ... -> (display 'ok) (newline)
|#
テンプレート部分ではパターン部分で使用した識別子が変数のように束縛される。_はプレースホルダーなのでテンプレート部分で使用しても置き換えが発生しない点に注意したい。上記のマッチ結果を踏まえて展開してみる。
(if test (begin expr ...))

;; test     -> (assq 'a lst)
;; expr ... -> (display 'ok) (newline)
(if (assq 'a lst) (begin (display 'ok) (newline))
syntax-rulesの感じは掴めただろうか?パターンマッチで入力式を分解し、テンプレートを置換して展開式を得る。基本はこれだけである。

設問 7.1
unlessは与えられた条件が偽であった際に続く式を評価する構文である。これをsyntax-rulesを使って実装せよ。

健全性


Schemeのマクロは健全なマクロを定義している。では、健全なマクロとはどういうものだろうか?簡単に言えばマクロ定義内で束縛された変数とマクロの外側で束縛された変数が衝突しないというものである。例えば上記のwhenの定義を以下のように変更してみよう:
(define-syntax when
  (syntax-rules ()
    ((_ test expr ...)
     (let ((t test))
       (if t
           (begin expr ...))))))
whenの内部でtが束縛されている。ではこのwhenを以下のように使ってみよう。
(let ((t (list 't)))
  (when (equal? t '(t))
    (display t) (newline)))
最も外側のlettが束縛されているが、実行結果は(t)が表示されるはずだ。当たり前のように思うかもしれないが、syntax-rulesが非健全なマクロであった場合、#tが表示されることになる。

では健全なマクロの何がうれしいのだろう?上記程度のものであれば、マクロの書き手が気をつければよいだけなのだが、以下のような例だと以下に書き手が気をつけても非健全なマクロでは変数の衝突が避けられない。
(define-syntax unless
  (syntax-rules ()
    ((_ test expr ...)
     (cond (test #f)
           (else expr ...)))))

(let ((else #f))
  (unless else
    (display "hygienic macro!") (newline)))
この例では、マクロ内でcond構文とelse補助構文を使っているが、これが非健全なマクロだった場合、letで束縛したelseunlessマクロ展開後の式に含まれるelseと衝突する。もちろん、マクロ使用者が気をつければいいのだが、マクロ展開器自体がこの問題を解決した方がプログラムの規模が大きくなった際に意図しない挙動を起こす可能性を減らすことができる。極力プログラムの処理に集中できるという点は健全なマクロの利点の一つになるだろう。

コラム:自由識別子
Schemeのマクロはここで記述した以上に奥が深い。例えば、syntax-rulesは補助構文のキーワードを受け取ることができる。ではこの識別子の比較はどのように行われているのだろうか?例としてelse補助構文を考えてみよう。上記の例であれば、単にシンボルの比較を行っただけでは同一のものとして扱われてしまう。そこでSchemeの規格(R5RS以降)では識別子が同一の束縛を指すかどうかで判別するように既定している。R6RSでは識別子が同一の束縛を指しているかどうかをチェックする手続きとしてfree-identifier=?がある。R7RSにこの手続きが存在しないのは、R7RSでは高レベルマクロしか既定していないからである。上記のunlessではletで束縛した識別子とマクロ内で定義された識別子が別のものを指すので展開後の式ではelseは別物として扱われるのである。

設問 7.2
Common Lispではaifと呼ばれるマクロが存在する。このマクロをsyntax-rulesを使って記述することは可能であろうか?

No comments:

Post a Comment