variable capture

マクロの(意図せぬ)変数捕捉について。

ケース1 : 引数がうああ! ってなる

;; こんなマクロを定義したとする
CL-USER> (defmacro swap (var1 var2)
           `(let ((temp ,var1))
             (setq ,var1 ,var2)
             (setq ,var2 temp) ))
SWAP

CL-USER> (setq a 10 b 20)
20
CL-USER> (swap a b)
10
CL-USER> (list a b)
(20 10) ;; やったぜ!

;; また入れ替えてやるぜ!
CL-USER> (setq temp 10 temp2 20)
20
CL-USER> (swap temp temp2)
20
CL-USER> (list temp temp2)
(10 20) ;; やtt・・・あれ?!

という現象が起きる。世界に一つだけの花とか歌ってる場合ではないみたいだ。
どういうことが起きているかは、マクロを展開してみると分かる。

CL-USER> (macroexpand '(swap temp temp2))
(LET ((TEMP TEMP)) (SETQ TEMP TEMP2) (SETQ TEMP2 TEMP))
T

まるでTEMPのバーゲンセールだ。
letでtempを定義しているので、let内では新しいtempが参照されちゃう。
なので、
(setq a b) (setq b a) といったことが起きている。

イディオム

ちなみに、Common Lispにはrotatefというマクロがある。

CL-USER> (let ((a 1) (b 2) (c 3) (d 4))
           (rotatef a b c d)
           (list a b c d) )
(2 3 4 1) ;; 回転した!

これに2つだけ渡すとswapの役割を果たすから、わざわざswapを定義したりはしない。

CL-USER> (let ((a 1) (b 2))
           (rotatef a b)
           (list a b) )
(2 1)

イディオム豆知識でございました。節分近いしね!


ケース2 : 自由変数が!環境が!ああ! ってなる

マクロはその場に展開されるということを考えると何も不思議はないんだけど、それでもこのような事態と遭遇するかもしれない。

CL-USER> (defvar counter 0) ;; グローバル変数
COUNTER
CL-USER> (defmacro countup ()
           `(incf counter) )
COUNTUP
CL-USER> (countup)
1
CL-USER> counter
1 ;; やったぜ!

;; 10回*を表示させつつカウントアップするぜ!
CL-USER> (dotimes (counter 10)
           (format t "*")
           (countup) )
***** ;; あれ?
NIL

CL-USER> counter
1 ;; !!!

という現象が起きる。
予想通りですか?そうですか。


邪気眼を使ってマクロ展開後の姿を映してみると、

CL-USER> (dotimes (counter 10)
           (format t "*")
           (incf counter) )

こうなっていることが分かる。
予想通りdotimesで用意したcounter変数をインクリメントしているのでした。


対策

ケース1はgensymを使って回避する。

CL-USER> (defmacro swap (var1 var2)
           (let ((temp (gensym)))
             `(let ((,temp ,var1))
               (setq ,var1 ,var2)
               (setq ,var2 ,temp) )))
SWAP

CL-USER> (setq temp 10 temp2 20)
20
CL-USER> (swap temp temp2)
10
CL-USER> (list temp temp2)
(20 10) ;; やったぜ!

CL-USER> (macroexpand '(swap temp temp2))
(LET ((#:G4397 TEMP)) (SETQ TEMP TEMP2) (SETQ TEMP2 #:G4397))
T

gensym関数が重複しないシンボルを作成してくれるので、そのシンボルをtempの値に束縛。
これでようやく花屋の店先に並んだ綺麗な花を見ることができます。


ケース2は、「気をつけろ!」に尽きる・・・かな?
例にあげたプログラムでいえば、defvarで宣言する変数は *var* のようにアスタリスクで挟むのが慣例らしいので、まず名前を見直す。あとは、パッケージを分割するとか。
ちなみにこのマクロを関数にしても同じ問題に遭遇するので*1、やはり名前を見直すのが一番かな。


その他の、関数は大丈夫だけどマクロだと問題を起こす例:

CL-USER> (defun my-print (x) (format t "~a !~%" x))
MY-PRINT
CL-USER> (defun foo (x) (my-print x))
FOO
CL-USER> (defmacro bar (x) `(my-print ,x))
BAR

CL-USER> (labels ((my-print (x) (format t "[~a]" x)))
           (foo 10)
           (bar 20) )
10 !
[20]
NIL


以上、変数捕捉でした。


参考:
On Lisp (9章)

*1:グローバル変数の宣言をdefvarではなくsetqにすると関数版は上手くいく。defvarはダイナミックスコープであるため。