マクロ定義の練習

今回はid:yagiey:20100126:1264525307で紹介したレガシーなマクロに関して。っていうか前回もだったけど。
id:yagiey:20100213:1266088272で

「新たな変数名を作り出す」という要請に対しての解決策という形で紹介される伝統的なマクロを次回。

といってみたけど、「新たな変数名を作り出す」という話題は置いといて、もうちょっとマクロに慣れたい。


LISPに伝統的なS式を操作するマクロのSchemeでの定義法は

(define-macro (マクロ名 引数 ...)
  本体)

となる。引数は評価されず、引数として与えた式のまま本体で使われる。
マクロ展開で得られるものは、評価結果じゃなくて引数のS式を変形したS式。


...といっても、感覚がつかめない。ってことで、

  • 引数は評価されない
  • 値を返すのではなく、マクロ展開結果のプログラムをS式で返す

という実感を得るために、いくつか実験をしてみる。あり得ないことをやっているというのは百も承知。
あらかじめ

(define nums '(1 2 3 4 5))

しといて、さて実験。

実験1

gosh> (define-macro (foo arg) arg)
#<undef>
gosh> (foo (car nums))
1
gosh> (macroexpand '(foo (car nums)))
(car nums)

前者は、引数として受け取ったS式をそのまま返すマクロなので、(foo (car nums))という式はマクロ展開により(car nums)という式になる。その後に評価されて1となる。
後者は、マクロ展開の様子。

実験2

gosh> (define-macro (foo arg) 'arg)
#<undef>
gosh> (foo (car nums))
*** ERROR: unbound variable: arg
Stack Trace:
_______________________________________
gosh> (macroexpand '(foo (car nums)))
arg

実引数がなんであれargというシンボルに展開するひどいマクロ。
マクロ展開後にargを評価しようとするけど、argが束縛されていないのでエラー。
どういう風に展開されたかmacroexpandで確認してみると、(foo (car nums))がargになってることが分かる。

実験3

gosh> (define-macro (foo arg) `,arg)
#<undef>
gosh> (foo (car nums))
1
gosh> (macroexpand '(foo (car nums)))
(car nums)

これは実験1の(define-macro (foo arg) arg)と同じだな。

実験4

gosh> (define-macro (foo arg) `,,arg)
*** ERROR: Compile Error: unquote appeared outside quasiquote: ,arg
"(stdin)":22:(define-macro (foo arg) `,,arg)

Stack Trace:
_______________________________________

gosh> (define-macro (bar arg) `,`,arg)
#<undef>
gosh> (bar (car nums))
1
gosh> (macroexpand '(bar (car nums)))
(car nums)

だんだん何をやっているか分からなくなってきたZE!

実験5

gosh> (define-macro (my-when test . body)
        `(if ,test (begin . ,body)))
#<undef>
gosh> (my-when (even? 2) 1 2 3 4)
4
gosh> (macroexpand '(my-when (even? 2) 1 2 3 4))
(if (even? 2) (begin 1 2 3 4))

これは前回出てきたmy-when。
ifとbeginはifとbeginというシンボルとして、しかしtestやbodyは引数として与えられたS式をそのまま使いたい。
そんな時に便利なのが準クオートだった。
カンマで評価してやれば(←ここで「評価」という言葉使うのがキモイ...ような気がする)、引数として与えられたS式にできる。
マクロの定義側では、展開された結果ifやbeginが束縛されているかどうかには関知しないわけだな(ifやbeginが束縛されていないなんてことはあり得ないけど)。

実験6

gosh> (define-macro (my-when test . body)
        (list 'if test (list* 'begin body)))
#<undef>
gosh> (my-when (even? 2) 1 2 3 4)
4
gosh> (macroexpand '(my-when (even? 2) 1 2 3 4))
(if (even? 2) (begin 1 2 3 4))

準クオート使わずにやってみた。そういやlist*なんてものもあったなぁ。
あれ?macroexpandの結果で、listやlist*は実行されてるけどそれで良いんだっけ?

実験7

gosh> (define-macro (hoge arg) (+ 1 2))
#<undef>
gosh> (hoge (car nums))
3
gosh> (macroexpand '(hoge (car nums)))
3

マクロ展開後には、(+ 1 2)っていうリストじゃなくて、(+ 1 2)を実行した結果になっている。あれ?


クオートも準クオートもしてないから、あたりまえか。
そうか、マクロ展開時にも式は評価されるのか。


例えば

  • 実験1:argが評価されたからargというシンボルではなく実引数になる。
  • 実験2:'argが評価されたからargというシンボルになる。

ってことだもんなー。ってことで、実験6の結果には納得しておく。

実験8

gosh> (define-macro (hoge arg) '(+ 1 2))
#<undef>
gosh> (hoge (car nums))
3
gosh> (macroexpand '(hoge (car nums)))
(+ 1 2)

今回はクオートしたので、(+ 1 2)というリストへ展開され、評価時に3になった。