Scheme:マクロ内でのループ
R5RSマクロでは、繰り返しをコーディングするのに再帰を使うしかない。 '...'を使ったパターンマッチで済まない場合 (例えば、一時変数を必要な だけ生成するとか) に、そのような再帰パターンを書くことになる。
実装パターン
部分的なループを実現したい場合、現状ではつぎの二つの方法が有力。
- 特別なマーカーを導入して、通常の使用ではマッチしないパターンをつくり出して 自分自身を再帰呼び出しする
- 再帰用の補助マクロの使用
特別なマーカーを使う
R5RSの例にも出てくる方法。
例えばこんなdefine-values。"helper"が渡されるパターンは内部の繰り返しに 当てられていて、そこで<name> ...それぞれに対応する一時変数<temp> ...を つくり出す (コードは Ken Dickeyによるもの)。
(define-syntax define-values (syntax-rules () ((define-values (<name> ...) <body> ...) (define-values "helper" (<name> ...) () (<name> ...) <body> ...) ) ((define-values "helper" () (<temp> ...) (<name> ...) <body> ...) (begin (define <name> #f) ... (call-with-values (lambda () <body> ...) (lambda (<temp> ...) (set! <name> <temp>) ... ) ) )) ((define-values "helper" (<var1> <var2> ...) <temp>s (<name> ...) <body> ...) (define-values "helper" (<var2> ...) (<temp> . <temp>s) (<name> ...) <body> ...) ) ))
この方法の利点は、マクロがself-containedにできること。 でもどうもad hocな感じは免れない。また、ユーザがうっかりか故意にか
(define-values "helper" ...)
と呼び出してしまったらどうなるんだ、という問題もある。
補助マクロを使う
この例はGaucheのdefine-valuesの定義で、ループ用にdefine-values-sub という補助マクロを導入している。
(define-syntax define-values (syntax-rules () ((_ (var ...) expr) (define-values-sub () (var ...) (var ...) expr)) ((_ . else) (syntax-error "malformed define-values" (define-values . else))) )) (define-syntax define-values-sub (syntax-rules () ((_ (tmp ...) () (var ...) expr) (begin (define var (undefined)) ... (receive (tmp ...) expr (set! var tmp) ... (undefined)))) ((_ (tmp ...) (v v2 ...) (var ...) expr) (define-values-sub (tmp ... tmp1) (v2 ...) (var ...) expr)) ))
この方式の欠点は、トップレベルに補助マクロも見えてしまうこと。
Gaucheの場合はmoduleがあるので、これ全体をmoduleに入れてdefine-valuesだけ exportしておけば、ユーザからは補助マクロの方は見えない (Gaucheではdefine-syntaxの参照等価性はmoduleも考慮するので、 define-valuesの展開結果に表れるdefine-values-subはdefine-valuesの 「定義環境」で参照される。そのため、ユーザがdefine-valuesを呼ぶ 環境でdefine-values-subが見えている必要はない。 define-macroにはそういう性質が無いので、define-macroの展開結果から 呼んでいるマクロや補助関数はマクロの使用環境からも見えている必要がある)
改善案
letrec-syntax
ふつうのdefine/letとの対称性を考えると、 こんなことが出来てもよさそうな気もする。
(define-syntax define-values (letrec-syntax ((define-values-sub (syntax-rules () ((_ (tmp ...) () (var ...) expr) (begin (define var (undefined)) ... (receive (tmp ...) expr (set! var tmp) ... (undefined)))) ((_ (tmp ...) (v v2 ...) (var ...) expr) (define-values-sub (tmp ... tmp1) (v2 ...) (var ...) expr)) )) ) (syntax-rules () ((_ (var ...) expr) (define-values-sub () (var ...) (var ...) expr)) ((_ . else) (syntax-error "malformed define-values" (define-values . else))) )))
- Rui: マクロを適用するコンテキストに存在しないidentifierを使うことができなくてよいのでは。Syntax Abstraction in Schemeで説明されているsyntax-caseだと、syntax-caseのtemplateにリテラルに現れる識別子でマクロ定義の外側の変数を参照できる理由は、それが変数をクローズするからではなく、letやlambdaが同名の識別子のbinding nameを改名するからのようです。例を挙げると、
(let ((a 1)) (let-syntax ((foo (syntax-rules () ((_ b) (cons a b))))) (let ((a 2)) (foo a))))
これが(1 . 2)になるのは、syntax-rulesが外側のaをクローズしているからではなく、aが適当に(let ((g0 1)) (let ((g1 2)) (cons g0 g1)))
というように改名されているからだという考え方です。これだとマクロが適用される場所で変数名を改名しても参照できないものへの参照はエラーになります(モジュールはまた別なんでしょうけど)。ここでは、define-valuesのsyntax-rulesが適用されるとき、それが適用される場所からdefine-values-subを参照できません。syntax-rulesではdefine-syntaxとsyntax-rulesの間になにかをおくことはできないのでどちらにせよ問題は表れませんが、これを許すことにすると、やっぱり改名でマクロを説明できなくなるのは同じなんではないかと思います。 - 直接は関係ないですが、R5RSのマクロいえば、Al Petrofskyの記事はおもしろかったです。これでhygienicなマクロに興味を持ちました。syntax-caseの論文はgoogleすれば出てきますが、たとえばここ。GuileやMzSchemeでも使えるようです。
- teranishi: (... template)が使えればRuiさんのおっしゃるような問題が起こらないように書けるのではないでしょうか。
(define-syntax define-values (syntax-rules () ((_ (var ...) expr) (begin (define var (undefined)) ... (letrec-syntax ((define-values-sub (syntax-rules () ((_ (tmp (... ...)) () (var (... ...)) expr) (receive (tmp (... ...)) expr (set! var tmp) (... ...) (undefined))) ((_ (tmp (... ...)) (v v2 (... ...)) (var (... ...)) expr) (define-values-sub (tmp (... ...) tmp1) (v2 (... ...)) (var (... ...)) expr)) )) ) (define-values-sub () (var ...) (var ...) expr))) ((_ . else) (syntax-error "malformed define-values" (define-values . else))) ))
- これだと展開した結果のdefineがletrec-syntaxでくくられるので、トップレベル定義にならないような。letrec-syntaxのinternal defineということに解釈されそうです。
- teranishi: 失礼。そのとおりです。しかも、define-values-sub内の自由識別子がトップレベルの環境を見てくれないかも…(これは勘違いだったようです。Gaucheで実験して問題ありませんでした)
- ん、いや、(define var (undefined)) ... をletrec-syntaxの前に出すとうまくいきそうではないですか?
- teranishi: ありがとうございます。ソースに反映しました。しかし、この場合はなんとかうまくいきましたが、一般の場合に適用するのは無理そうです。