マクロを書くときには注意が余分に必要だ. 関数は自分のレキシカルな領域に孤立しているが, マクロは,呼出側のコード内に展開されるので, 注意深く書かないと不愉快な驚きをもたらすことがある. 第9章で扱った変数捕捉はその最たるものだ. この章ではマクロを定義する際に避けるべき問題をさらに議論する.
前の章ではfor
の不適切な定義例をいくつか示した.
第\ref{fig:ControlingArgEval}図にはさらに2例を,比較用の適切な定義と共に示した.
(defmacro for ((var start stop) &body body) (let ((gstop (gensym))) `(do ((,var ,start (1+ ,var)) (,gstop ,stop)) ((> ,var ,gstop)) ,@body)))複数回の評価を起こし得る:
(defmacro for ((var start stop) &body body) `(do ((,var ,start (1+ ,var))) ((> ,var ,stop)) ,@body))評価の順番が間違っている:
(defmacro for ((var start stop) &body body) (let ((gstop (gensym))) `(do ((,gstop ,stop) (,var ,start (1+ ,var))) ((> ,var ,gstop)) ,@body)))\caption{引数評価の制御} \label{fig:ControlingArgEval}
捕捉を起こすことはないが,2番目にはバグがある.
これの展開形はstop
として渡された式を反復の度に評価してしまう.
一番運のよかった場合でも,この種のマクロは非効率で,1回行えばよいことを繰り返している.
stop
に副作用があれば,このマクロが実際に間違った結果をもたらすかも知れない.
例えば次のループはいつまでも終了しない.
ゴールが反復の度に遠のいてゆくからだ.
> (let ((x 2)) (for (i 1 (incf x)) (princ i))) 12345678910111213...
for
のようなマクロを書くときには,
マクロの引数は式であって値ではないことを忘れてはいけない.
それらが展開形内に現れる場所によっては複数回評価されることもある.
上の場合,解決策は式stop
が返した値に変数を束縛し,
繰り返しの間にはその変数を参照することだ.
マクロは,明確に反復を意図していなければ,
式がきっかりマクロ呼び出しに出て来る回数だけ評価されるようにしておくべきだ.
この規則の当てはまらない明らかな例がある:
Common Lispのor
は,全ての引数を必ず評価するようになったら
ずっと不便になってしまうだろう(それではPascalのor
構文だ).
しかしこのような場合では何回評価が行われるかは分かっている.
上の2番目のfor
ではそうではない.
式stop
が複数回評価されると想定するような理由はないし,実際そうであるべき理由もない.
2番目のfor
のようにして書かれたマクロは過誤の産物であることが一番多い.
意図せぬ複数回の評価はsetf
の上に構築されたマクロにとっては特に難しい問題だ.
Common Lispはそのようなマクロを書き易くするための幾つかのユーティリティを提供している.
問題と解決策は第12章で扱う.
式が評価される順番は式が評価される回数程重要ではないが,時々問題になる. Common Lispの関数呼び出しでは,引数は左から右の順で評価される.
> (setq x 10) 10 > (+ (setq x 3) x) 6
マクロについてもそうするのがよい習慣だ. マクロでは常に確かに式がマクロ呼び出し内と同じ順で評価されるようにするべきだ.
第\ref{fig:ControlingArgEval}図では,3番目のfor
にも微妙なバグがある.
仮引数stop
はstart
より前に評価される.
マクロ呼び出しでは逆の順番になっているというのに.
> (let ((x 1)) (for (i x (setq x 13)) (princ i))) 13 NIL
このマクロは,時間が逆戻りしたかのようなdisconcerting印象を与える.
字面の上では式start
が最初に出て来るにも関わらず,
式stop
の評価が式start
の返す値に影響しているのだ.
適切に定義されたfor
では,引数は確かに出て来る順で評価されるようになっている.
> (let ((x 1)) (for (i x (setq x 13)) (princ i))) 12345678910111213 NIL
これならx
を式stop
内で設定しても,前の引数が返す値には何の影響もない.
上に示した例はこのために作ったものだが,
この種の問題が顕在化する状況は存在し,そしてこのようなバグは発見が極めて難しい.
マクロのある引数の評価が別の引数が返す値に影響するようなコードを書く人はまずいないだろう.
しかし人は意図的には決してやらないことでも誤ってやってしまうことがあるものだ.
ユーティリティは,意図された通りに使われれば正しく機能するだけでなく,
バグを隠蔽してもいけない.
上の例のようなコードを書く人がいたら,それはきっと間違って書いたのだろう.
しかし適切に定義されたfor
は容易に誤りに気付かせてくれる.
Lispは,マクロ展開を生成するコードは 第3章で論じた意味で純粋に関数的であるものと予期している. 展開を行うコードは引数として渡された式にのみ依存すべきで, 値を返す他には周囲の世界に影響しようとすべきではない.
\textsf{CLtL2}にもあるように(p. 685), コンパイル済みコード内でのマクロ呼び出しは 実行時に再展開されることはないと思っておくのが安全だ. そうでなければ,Common Lispはいつの時点で, またはどれほどの頻度でマクロ呼び出しが展開されるのか保証できなくなってしまう. マクロの展開結果がそのどちらかによって変わることがあれば,それは誤りと見なされる. 例えば,あるマクロが使われた回数を数えたいとしよう. 単にソースに検索をかければよい訳ではない. そのマクロがプログラムの生成したコード内で呼ばれているかも知れないからだ. だから次のようなマクロを定義したくなる.
(defmacro nil! (x) ; 誤り (incf *nil!s*) `(setf ,x nil))
この定義ではグローバル変数*nil!s*
がnil!
が展開される度に1だけ増加する.
しかしこの変数の値からnil!
の呼ばれた回数が分かると思ったら間違いだ.
あるマクロ呼び出しは複数回展開され得る(実際しばしばそうだ).
例えばソースコードの変換を行うプリプロセッサは,式内のマクロ呼び出しを,
それを変換すべきかどうか判断する前に展開しなければならないかもしれない.
一般則としては,展開を行うコードは引数以外の何にも依存すべきでない. だから例えば展開形を文字列から作り出すようなどのマクロも, 展開時に何のパッケージ内にいるかについて勝手に予想しないよう注意すべきだ. 次の簡潔だがかなり病的な例は,
(defmacro string-call (opstring &rest args) ; 誤り `(,(intern opstring) ,@args))
オペレータの印字名をとってそのオペレータへの呼び出しを展開するマクロを定義している.
> (defun our+ (x y) (+ x y)) OUR+ > (string-call "OUR+" 2 3) 5
intern
は文字列を取り,対応するシンボルを返す.
しかしオプショナル引数のパッケージを省くと,カレント・パッケージが使われる.
よって展開形はそれが生成されるときのパッケージに依存することになる.
our+
がそのパッケージで可視でない限り,展開形は未定義関数への呼び出しになる.
MillerとBensonの\emph{Lisp Style and Design}は
展開コードの副作用から起きる問題の特別に汚い例に言及している.
Common Lispでは\textsf{CLtL2}にもあるように(p. 78),
&rest
引数に束縛されたリストが新しく生成されたものである保証はない.
それはプログラム内のどこかのリストと構造を共有しているかも知れない.
その結果,&rest
引数は破壊的に操作してはいけないことになる.
同時に他の何に変更を加えているか分からないからだ.
この可能性は関数とマクロ両方に影響する.
関数についてはapply
を使うと問題が顕在化する.
Common Lispの規格に沿った処理系の実装では次の問題が起こり得るのだ.
引数のリストの末尾にet alを付け加える関数et-al
を定義したいとしよう.
(defun et-al (&rest args) (nconc args (list 'et 'al)))
この関数は普通に呼ぶ限りでは適切に動作するように思える:
> (et-al 'smith 'jones) (SMITH JONES ET AL)
しかしそれをapply
経由で呼び出すと,既存のデータ構造が変更される可能性がある:
> (setq greats '(leonardo michelangelo)) (LEONARDO MICHELANGELO) > (apply #'et-al greats) (LEONARDO MICHELANGELO ET AL) > greats (LEONARDO MICHELANGELO ET AL)
少なくともCommon Lispの規格に沿った処理系ではこうなる可能性がある. 今のところ実際にはそのような処理系はないようだが.
マクロでは危険度が大きい.
&rest
引数に変更を加えるマクロはそれによってマクロ呼び出しに変更を加える可能性がある.
言い替えれば,うっかり自己修正的なプログラムができてしまいかねないということだ.
現実的な危険性もある
---これは既存の処理系で起こったことなのだ.
何かを&rest
引数にnconc
するマクロを書き
\footnote{`',(foo)
は`(quote ,(foo))
と等価な表現だ.},
(defmacro echo (&rest args) `',(nconc args (list 'amen)))
次にそれを呼び出す関数を書く:
(defun foo () (echo x))
広く使われているCommon Lisp処理系では,次のような結果になる.
> (foo) (X AMEN AMEN) > (foo) (X AMEN AMEN AMEN)
foo
は誤った結果を返すだけでなく,実行する度に結果は異なる.
マクロが展開される度にfoo
の定義が変更されているからだ.
あるマクロ呼び出しが複数回展開されることについて先に論じた点についても,
この例から分かることがある.
この特定の処理系では,foo
の1回目の呼び出しは2個のamenから成るリストを返す.
何かの理由でこの処理系はマクロ呼び出しをfoo
の展開時に1回展開し,
そしてその後呼び出される度にも1回ずつ展開したのだ.
echo
は次のように定義した方が安全だ.
(defmacro echo (&rest args) `'(,@args amen))
これはコンマ・アットはnconc
でなくappend
と等価だからだ.
このマクロを再定義した後は,
foo
はコンパイルされていなくとも再定義しなければならない.
前のヴァージョンのecho
がそれを書き換えてしまったからだ.
??? ここから??ENDまでの行が挿入か削除されたようです
マクロでは,同様の危険があるのは&rest
引数だけではない.
リストであるマクロの引数はいずれも変更しないでおくべきだ.
引数のどれかに変更を加えるマクロとそれを呼ぶ関数を定義すると,
(defmacro crazy (expr) (nconc expr (list t))) (defun foo () (crazy (list)))
呼出側関数のソースコードは変更されてしまう可能性がある. ある処理系では1回目に呼び出したときにそうなった.
> (foo) (T T)
これはコンパイラとインタプリタ両方で起きたことだ.
要するに,引数のリスト構造を破壊的に変更することでコンシングを避けようとしないことだ. 出来上がったプログラムは,例え動作したとしても可搬性を持たないだろう. 可変個の引数を取る関数内でコンシングを避けたいなら, 解決策の一つはマクロを使い,コンシングをコンパイル時にずらす方法だ. マクロをこのように使うことについては,第13章を参照すること.
またマクロ展開を行うコードの返した式がクォート付きリストを含むときは, それに破壊的操作を行うことも避けるべきだ. これはマクロの本質的な制限ではなく,第3.3節で大まかに触れた原則の一例だ.
関数を再帰的に定義するのが自然なときがある. 次のような関数は,本質的に再帰的な性質を持っている.
(defun our-length (x) (if (null x) 0(1+ (our-length (cdr x)))))
上の定義は,等価な反復版よりも(おそらく遅いだろうが)自然に思える.
(defun our-length (x) (do ((len 0 (1+ len)) (y x (cdr y))) ((null y) len)))
再帰的でもなく,相互再帰的な関数の集合の一部でもない関数は,
第7.10章で述べた簡単な方法によりマクロに変換できる.
しかしただ逆クォートとコンマを挿入するだけでは再帰的関数は変換できない.
組込み関数nth
を例に取ってみよう.
(話を簡単にするため,このnth
はエラーチェックをしないものとする.)
第\ref{fig:MistakenRecursiveFunc}図には
nth
をマクロとして定義しようとして間違えた例を示した.
表面的にはnthb
はntha
と等価に見えるが,
nthb
を使ったコードはコンパイルに通らない.
これはマクロ呼び出しの展開が終了しないためだ.
(defun ntha (n lst) (if (= n 0) (car lst) (ntha (- n 1) (cdr lst))))コンパイルできないもの:
(defmacro nthb (n lst) `(if (= ,n 0) (car ,lst) (nthb (- ,n 1) (cdr ,lst))))\caption{誤って再帰関数と同じように捉えてしまった例.} \label{fig:MistakenRecursiveFunc}
一般的に,マクロが他のマクロへの参照を含むことは,展開がどこかで終了する限りは問題ない.
nthb
の問題は,nthb
のどの展開形もnthb
自身への参照を含む点だ.
関数版のntha
は終了する.
それはn
の値について再帰的なのだが,この値は再帰の度に減少するからだ.
しかしマクロ展開は式にしかアクセスがなく,その値までは分からない.
コンパイラが,例えば(nthb x y)
を展開しようとすると,最初の展開では
(if (= x 0) (car y) (nthb (- x 1) (cdr y)))
が作られ,これは更に次のようになる.
(if (= x 0) (car y) (if (= (- x 1) 0) (car (cdr y)) (nthb (- (- x 1) 1) (cdr (cdr y)))))
こうして無限ループに陥ってしまうのだ. マクロが自分自身の呼び出しを展開することに問題はないが, それはいつまでも続かないときの話だ.
nthb
のような再帰的マクロの危険な点は,インタプリタでは適切に機能することだ.
とうとう動作するようになったプログラムをコンパイルしようとしたとき,
それがコンパイルを通りすらしないことになる.
それだけでなく,問題が再帰的マクロによることは普通分からない.
コンパイラはただ無限ループに陥り,そこで何が悪いのか考える羽目になる.
上の場合,ntha
は末尾再帰的だ.
末尾再帰的関数は容易に反復形に変換でき,それはマクロのモデルに使える.
nthb
のようなマクロはこうして書ける.
(defmacro nthc (n lst) `(do ((n2 ,n (1- n2)) (lst2 ,lst (cdr lst2))) ((= n2 0) (car lst2))))
原則的にはマクロで再帰関数を複製することは不可能ではない. しかし複雑な再帰関数の変換は難しかったり,不可能ですらあるときもある.
マクロの用途によっては,代わりにマクロと関数の組合せを使うことで十分なときもある.
第\ref{fig:TwoWayFixProb}には再帰的マクロのようなものを作る方法を2通り示した.
nthd
で示される1番目の戦略は,単にマクロを再帰関数の呼び出しへ展開させることだ.
例えばマクロが引数にクォートを付ける手間を省くためだけに使われているのなら,
この手法で十分だろう.
(defmacro nthd (n lst) `(nth-fn ,n ,lst)) (defun nth-fn (n lst) (if (= n 0) (car lst) (nth-fn (- n 1) (cdr lst)))) (defmacro nthe (n lst) `(labels ((nth-fn (n lst) (if (= n 0) (car lst) (nth-fn (- n 1) (cdr lst))))) (nth-fn ,n ,lst)))\caption{問題の2通りの修正方法.} \label{fig:TwoWayFixProb}
マクロが必要な理由が,その展開形を丸ごと呼出側のレキシカルな環境に挿入したいということなら,
nthe
に例示した方法の方ががよいだろう.
組込みの特殊式labels
(第2.7節を参照)がローカルな関数定義を作っている.
nthc
の展開形はいずれもグローバル関数nth-fn
を呼んでいたが,
nthe
の展開形はそれぞれの内部に同様な関数を持っている.
再帰関数を直接マクロに変換できなくても,展開形が再帰的に生成されるマクロを書くことはできる.
マクロを展開する関数はLispの通常の関数であって,もちろん再帰的になり得る.
例えば組込みのor
を独自に定義するときには,
再帰的な展開関数を使うことになるだろう.
第\ref{fig:RecExpansionFunc}図には,
or
のための再帰的な展開関数を定義する方法を2通り示した.
マクロora
は展開形を作るために再帰関数or-expand
を呼び出す.
これは適切に動作する.また等価なorb
も同様だ.
orb
は再帰的だが,
マクロの引数そのものについて再帰的であり(これは展開時にアクセスできる),
その値について再帰的なのではない(こちらはアクセスできない).
展開形がorb
そのものへの参照を含むようにも見えるが,
ある1段階のマクロ展開で生成されたorb
の呼び出しは
次の段階でlet
に置き換えられ,
展開の最終形では入れ子になったlet
のスタックしか残らない.
(orb x y)
は次のコードと等価なコードに展開される.
(let ((g2 x)) (if g2 g2 (let ((g3 y)) (if g3 g3 nil))))
実際ora
とorb
は等価であり,どちらを使うかは個人的な好みの問題に過ぎない.
(defmacro ora (&rest args) (or-expand args)) (defun or-expand (args) (if (null args) nil (let ((sym (gensym))) `(let ((,sym ,(car args))) (if ,sym ,sym ,(or-expand (cdr args))))))) (defmacro orb (&rest args) (if (null args) nil (let ((sym (gensym))) `(let ((,sym ,(car args))) (if ,sym ,sym (orb ,@(cdr args)))))))\caption{再帰的展開関数.} \label{fig:RecExpansionFunc}