インタプリタ関数ではマクロ呼び出しが評価されるたびに展開されるのに対し、コンパイル済み関数では(コンパイル時に)一度だけ展開されるという点から、時折問題が生じることがあります。マクロ定義に副作用がある場合、マクロが展開される回数によって挙動が異なります。
したがって、自分が何をしているのかを十分に理解していない限り、マクロ展開の計算における副作用は避けるべきです。
ある特殊な副作用だけは避けられません。それはLispオブジェクトの構築です。ほぼすべてのマクロ展開には構築されたリストが含まれており、それこそがほとんどのマクロの本質です。通常は安全ですが、唯一注意が必要なケースがあります。それは、構築するオブジェクトがマクロ展開内のクォートされた定数の一部となっている場合です。
コンパイル時にマクロが一度だけ展開される場合、オブジェクトはコンパイル中に一度だけ生成されます。一方、インタプリタ実行では、マクロ呼び出しが実行されるたびにマクロが展開されるため、その都度新しいオブジェクトが生成されます。
ほとんどのクリーンなLispコードでは、この違いは問題になりません。問題になるのは、マクロ定義によって構築されたオブジェクトに対して副作用を引き起こす場合のみです。したがって、トラブルを避けるには、マクロ定義によって構築されたオブジェクトへの副作用を避けるようにしてください。このような副作用がどのようにトラブルを招くか、以下に例を挙げます。
(defmacro empty-object () (list 'quote (cons nil nil)))
(defun initialize (condition)
(let ((object (empty-object)))
(if condition
(setcar object condition))
object))
initialize が解釈される場合、initialize が呼び出されるたびに新しいリスト (nil) が構築されます。したがって、呼び出し間で副作用が保持されることはありません。initialize がコンパイルされる場合、コンパイル時にマクロ empty-object が展開され、単一の定数 (nil)
が生成されます。この定数は、initialize が呼び出されるたびに再利用され、変更されます。
このような病的なケースを避ける一つの方法は、empty-objectをメモリ割り当ての構文ではなく、一種の「面白い定数」として捉えることです。'(nil)のような定数にsetcarを使うことはありませんから、当然(empty-object)に対しても使うことはないでしょう。