マクロ展開はコンパイル時に行われるため、デバッグが難しいです。 マクロのデバッグに悩まされるのを避ける最良の方法は、どうしても必要でない限り マクロを書かないこと、そして書くハメになったとしてもできる限り単純なものにすることです。
しかし、時には怪しげなマクロを解きほぐす必要に迫られるという不運に見舞われるかもしれません。 Gaucheにはその助けになるツールがいくつかあります。
• マクロ展開をトレースする: | ||
• マクロを自分で展開する: |
マクロトレースは、選択したマクロのマクロ展開器の入力と結果を表示する機能です。
例えば、次のマクロを定義したとしましょう。これはR7RSの7.3節にあるletrec
の
定義とだいたい同じです:
(define-syntax my-letrec (syntax-rules () [(_ ((var init) ...) body ...) (my-letrec "tmps" (var ...) () ((var init) ...) body ...)] [(_ "tmps" () (tmp ...) ((var init) ...) body ...) (let ((var 'undefined) ...) (let ((tmp init) ...) (set! var tmp) ... body ...))] [(_ "tmps" (x y ...) (tmp ...) binds body ...) (my-letrec "tmps" (y ...) (newtmp tmp ...) binds body ...)]))
このmy-letrec
マクロは、"tmps"
をタグにしてループすることで
一時変数を作るというイディオムを使っています。my-letrec
をトレースすることで、
このマクロの展開の様子をステップごとに見ることができます:
gosh> (trace-macro 'my-letrec) (my-letrec) gosh> (my-letrec [(ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))] (ev? 3)) Macro input>>> (my-letrec ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" (ev? od?) () ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" (ev? od?) () ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" (od?) (newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" (od?) (newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" () (newtmp.1 newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" () (newtmp.0 newtmp.1) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (let ((ev? (quote undefined)) (od? (quote undefined))) (let ((newtmp.0 (^n (if (= n 0) #t (od? (- n 1))))) (newtmp.1 (^n (if (= n 0) #f (ev? (- n 1)))))) (set! ev? newtmp.0) (set! od? newtmp.1) (ev? 3))) #f
上に示した例において、gosh>
プロンプトの次にあるS式がユーザがタイプしたものです。
他はすべて、Macro input
とMacro output
のS式を含め、
Gaucheの出力です。
Macro input
の後に示されるS式がマクロ展開器の入力で、
Macro output
の後がその展開結果です。実際のマクロ展開結果は
構文情報が付加されていますが、この出力では読みやすくするために構文情報は取り去られています。
ループの度に同じ名前(newtemp
)の変数が導入されていますが、
それらが異なる識別子として扱われていることが展開結果からわかると思います。
デバッグが終わったら、untrace-macro
を引数なしで呼んで
マクロトレースをオフにしましょう。マクロトレースが設定されていると、
すべてのマクロ展開においてオーバヘッドがかかります。
gosh> (untrace-macro) #f
現在のマクロトレース設定を得たり、変更したりします。 マクロトレース設定は次のいずれかの値を取ります。
#f
マクロトレースはオフです。これがデフォルトの設定です。
#t
全てのマクロの展開がトレースされます。
(name-or-pattern …)
名前がname-or-patternのいずれかにマッチするマクロがトレースされます。 ここで各name-or-patternは、シンボルか正規表現オブジェクトです。 シンボルの場合は名前が完全に一致するもの、正規表現の場合は 名前がそれに一致するものがトレースの対象となります。
引数なしで呼ばれたら、trace-macro
は設定を変えず、単に現在の設定を返します。
単一の真偽値を引数に渡した場合は、それをマクロトレースの設定値として、 更新された設定値を返します。
一つ以上のname-or-patternが渡されたら、それらが現在のマクロトレース設定に
追加されます。もし現在の設定が#t
なら設定は変わらないことに注意して
ください。全てのマクロは既にトレースされているからです。
更新された設定値を返します。
マクロトレースの設定値が#f
でない限り、すべてのマクロ展開にオーバヘッドがかかります。
マクロトレースを設定したままにしないようにしてください。
トレース情報は現在のトレース出力ポートに出力されます。 (ポート共通の操作参照)。
引数なしで呼ばれた場合、マクロトレースをオフにします。
1つ以上のname-or-patternが渡された場合は、現在のマクロトレース設定から
それらを除きます。ただし、現在のマクロトレース設定が#t
(全てのマクロをトレース)
の場合は個別にマクロを除外できません。
更新後のマクロトレース設定を返します。
form がリストで、その最初の要素が大域的にマクロに束縛された
変数であるならば、macroexpand-1
はそのマクロ変換子を実行し、
展開されたフォームを返します。そうでなければ、form をそのまま
返します。
macroexpand
は、formの一番外側の式がが展開できなくなるまで
macroexpand-1
を繰り返します。
(macroexpand
はformの一番外側以外のマクロは展開しません。
form内の全てのマクロを展開するにはmacroexpand-all
を使ってください。)
これらの手続きは、大域的に定義されたマクロを展開するために使うことが できます。
内部的には、衛生マクロは展開されるとformの中にあるシンボルを 構文情報でラップします。しかし、REPLでマクロを展開して確かめたい場合に その情報がついているととても読みにくくなります。 そこで、デフォルトではこれらの手続きは構文情報を取り除いて出力します。 マクロ内で導入された識別子については、必要に応じて名前の衝突を避けるリネームが 行われます。
下の例はmy-letrec
(定義はマクロ展開をトレースする参照) を
展開しています。マクロ展開で導入された変数 (newtemp) がリネームされています。
(macroexpand '(my-letrec [(ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))] (ev? 3))) ⇒ (let ((ev? (quote undefined)) (od? (quote undefined))) (let ((newtmp.0 (^n (if (= n 0) #t (od? (- n 1))))) (newtmp.1 (^n (if (= n 0) #f (ev? (- n 1)))))) (set! ev? newtmp.0) (set! od? newtmp.1) (ev? 3)))
env引数にモジュールを渡すと、そのモジュールをマクロ使用環境として
マクロが展開されます。envに#t
を渡すと、実行時のモジュールが
マクロ使用環境になります。これらの場合は、出力に構文情報が付加されたままになります。
macroexpand
の出力を他のマクロ展開結果にプログラム的に埋め込むといった
用途に使う場合には、構文情報を保ったままのフォームが必要になります。
form中にあるマクロを全て展開します。結果の中に残るのは、 関数呼び出しとGaucheの組み込み構文だけになります。
デフォルト、もしくはenvに#t
が渡された場合は、
formは実行時のモジュールのトップレベルにあると解釈されます。
モジュールをenvに渡せば、そのモジュールをマクロ使用環境として展開できます。
form中で導入されるローカル変数は全て、衝突を避けるためにリネームされます。
ローカル変数が全て固有の名前を持つようになるので、let
フォームはすべて
letrec
で表されます(let
による束縛が他の束縛をシャドウしないと
わかっていれば、let
をletrec
に置き換えても意味は変わりません)。
注意: もしform内で呼ばれているマクロが、他のモジュール中にあるグローバル
変数への参照を挿入した場合、現在の実装ではその情報は失われてしまいます。
いくつか、その問題を修正する方法は考えられるのですが(例えば他のモジュール中に
グローバル変数参照は識別子オブジェクトのまま残しておくとか、
with-module
フォームに変換するとか、
そういったケースのための特殊構文を導入するとか)、今のところどうするか
決まっていません。なので、現在のふるまいにあまり依存しないようにしてください。
今のところ、この手続きは、マクロの展開結果をインタラクティブに確かめる用途に
限って使うのが安全です。
(macroexpand-all '(letrec-syntax [(when-not (syntax-rules () [(_ test . body) (if test #f (begin . body))]))] (let ([if list]) (define x (expt foo)) (let1 x 3 (when-not (bar) (if x)))))) ⇒ (letrec ((if.0 list)) (letrec ((x.1 (expt foo))) (letrec ((x.2 '3)) (if (bar) '#f (if.0 x.2)))))