変数捕捉

マクロの弱点は,変数捕捉と呼ばれる問題だ. 変数捕捉はマクロ展開が名前の衝突を起こしたときに生じる. つまりあるシンボルが別のコンテキストの変数を参照してしまったときだ. うっかりして起きた変数捕捉が,ものすごく些細なバグにつながることもあり得る. この章では,どのようにそれを予測し,避けるかを扱う. しかしながら意図的な変数捕捉は便利なプログラミング技法であり, 第14章はそれを利用したマクロがほとんどだ.

マクロ引数の捕捉

意図しない変数捕捉を弱点に持つマクロとは,バグのあるマクロと言ってよい. そのようなマクロを書くのを防ぐには,変数捕捉がいつ起きるのかを正確に知らなければならない. 実際の変数捕捉の元を辿ると,以下の2通りの状況に行き着く. マクロ引数の捕捉とフリーシンボルの捕捉だ. マクロ引数の捕捉とは,マクロ呼び出しの引数として渡されたシンボルが, 不注意によってマクロ展開そのものによって生成された変数を参照してしまうことである. マクロforの,以下の定義を考えてみよう. これはPascalのfor構文のように,本体となる式の実行を繰り返すものだ.

(defmacro for ((var start stop) &body body)          ; 誤り
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
     ((> ,var limit))
     ,@body))

このマクロは初見では正しいように見える. それどころかちゃんと動作までするようだ.

> (for (x 1 5)
       (princ x))
12345
NIL

実際,誤りは微妙なもので,上の形のマクロを数百回使っても全て完璧に動作するかも知れない. しかし,下のように呼び出さなければ,の話だ.

(for (limit 1 5)
  (princ limit))

この式も上のものと同じ効果を持つと思える. しかしこれは何も表示しない.エラーを起こすのだ. 理由を知るために,その展開形を見てみよう.

(do ((limit 1 (1+ limit))
         (limit 5))
        ((> limit limit))
  (princ limit))

これで何が誤りを引き起こしたのかは明らかだ. マクロの展開形についてローカルなシンボルと,マクロに引数として渡されたシンボルとの間に, 名前の衝突があったのだ. 展開形がlimitを捕捉していた. 結局それが同じdoの中で2回現れることになり,エラーになったのだ. 変数捕捉によるエラーは稀だが,頻度が少ない分,質の悪いものだ. 先程の変数捕捉は比較的大人しい ---少なくとも,ここではエラーが起きた. しかし大抵は,変数捕捉を起こすマクロは, 何かがおかしいという兆候を何も示さずに誤った結果をもたらす. 下の場合では,

> (let ((limit 5))
    (for (i 1 10)
         (when (> i limit)
           (princ i))))
NIL

結果のコードはエラーを出さず,しかし何の動作もしない.

フリーシンボルの捕捉

起こる頻度は少ないが, 不注意からマクロ定義そのものが,マクロが展開された環境内の束縛を参照することがある. あるプログラムで,問題が起きたときユーザに警告を発する代わりに, 後ほど検討するために警告をリストに蓄えたいとしよう. ある人はマクロgripeを書いた. これは警告メッセージを取り,それをグローバルなリストwに付け加えるものだ.

(defvar w nil)

(defmacro gripe (warning)                        ; 誤り
  `(progn (setq w (nconc w (list ,warning)))
          nil))

別のある人は関数sample-ratioを書こうと思った. それは2つのリストの長さの比を返すものだ. また,どちらかのリストが1個以下の要素しか持たなければ, 代わりに nil を返し,同時に「統計的に意味の見出せない状況だ」と警告を発する. (実際の警告はもっと丁寧なのだろうが,警告内容はこの例にとってはどうでもよい.)

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
    (if (or (< vn 2) (< wn 2))
      (gripe "sample < 2")
      (/ vn wn))))

sample-ratiow = (b)という状態で呼ばれたら, 「引数の一つが要素を1個しか持っていない,統計的に無意味だ」 と警告を発しようとするだろう. しかしgripeの呼び出しが展開されると, sample-ratioが下のように定義されたかのような結果になる.

(defun sample-ratio (v w)
  (let ((vn (length v)) (wn (length w)))
    (if (or (< vn 2) (< wn 2))
      (progn (setq w (nconc w (list "sample < 2")))
             nil)
      (/ vn wn))))

ここでの問題は,gripeの使われたコンテキストでは wにはそれ自身のローカルな束縛があるということだ. 警告はグローバルな警告リストに保存されずに, sample-ratioの仮引数の1つの末尾にnconcされてしまう. 警告が失われるだけではない. リスト(b)は恐らくプログラムのどこかでデータとして使われるのだろうが, 後ろに余計な文字列がくっついてしまう.

> (let ((lst '(b)))
    (sample-ratio nil lst)
    lst)
(B "sample < 2")
> w
NIL

捕捉はいつ起きるのか

マクロ定義を見て次の2種類の捕捉から起き得る問題を予測できるようになりたいと, 多くのマクロ作者が願い続けてきた. 捕捉は微妙な問題で, 捕捉され得るシンボルがどのようにプログラムに障害をばら撒くかを全て予測できるようになるには, 幾らかの経験が要る. 幸運なことに,捕捉がプログラムにどのような悪影響を及ぼしているのかを考えなくとも, マクロ定義内の捕捉の起き得るシンボルを判別し,取り除くことができる. この節では,捕捉可能なシンボルを判別するための直截的な方法を示す. この章の残りの節では,そのようなシンボルを取り除く方法を説明する.

捕捉可能な変数を定義する規則は幾つかの耳慣れない概念に依存しており, それらを先に定義しておかなければならない.

フリー
シンボルsが式の中でフリーなままで現れるというのは, その式の中で変数として使われているが,式の中でその変数が束縛されていないときだ.

以下の式の中では, wxzはどれも式list内でフリーに現れており, 束縛を作っていない.

(let ((x y) (z 10))
  (list w x z))

しかし外側の式letxzに束縛を与えているので, letの中全体では,ywのみがフリーなまま現れている. 下の式の中では,

(let ((x x))
  x)

2個目に現れるxはフリーである ---xに対して作られた新しい束縛のスコープ内にはない.

骨格
マクロ展開の骨格は, 展開形そのものからマクロ呼び出しの引数の一部だったものを全て取り除いたものだ.

以下のようにfooが定義されていて,

(defmacro foo (x y)
  `(/ (+ ,x 1) ,y))

下のように呼ばれていたら,

(foo (- 5 2) 6)

これは下のような展開形を与える.

(/ (+ (- 5 2) 1) 6)

この展開形の骨格は,上の式の仮引数xyが入っていた所に穴を開けたものだ.

(/ (+                  1) )

これら2つの概念が定義されていれば, 捕捉され得るシンボルを判別するための便利な規則を述べることができる.

捕捉可能
何らかのマクロの展開形内でシンボルが捕捉可能だというのは, 以下の条件のどちらかが満たされるときである.
  1. マクロ展開の骨格内でフリーなまま現れているか,
  2. マクロに渡された引数が束縛または評価される骨格の一部に束縛されている.

幾つかの例からこの規則の表すものを見てみよう. 下のような一番単純な場合では,

(defmacro cap1 ()
  '(+ x 1))

xは骨格内でフリーなまま現れるだろうから,捕捉可能だ. これがgripe内のバグを引き起こしたものだ. 下のマクロでは,

(defmacro cap2 (var)
  `(let ((x ...)
              (,var ...))
         ...))

xは捕捉可能だ. それは,xが束縛されているのと同じ式にマクロ呼び出しの引数も束縛されるからだ. (それがforの失敗を引き起こした.) 同様に以下の2つのマクロでは,

(defmacro cap3 (var)
  `(let ((x ...))
         (let ((,var ...))
            ...)))

(defmacro cap4 (var)
  `(let ((,var ...))
         (let ((x ...))
            ...)))

どちらでもxは捕捉可能だ. しかし例えば下の場合のように, xの束縛と引数として渡された変数とがともに可視であるようなコンテキストがなければ,

(defmacro safe1 (var)
  `(progn (let ((x 1))
            (print x))
          (let ((,var 1))
            (print ,var))))

xは捕捉可能にはならない. 骨格に束縛されている変数が全て危険な訳ではない. しかしマクロ呼び出しの引数が骨格によって作られた束縛の中で評価されたら,

(defmacro cap5 (&body body)
  `(let ((x ...))
         ,@body))

そのように束縛された変数には捕捉の危険がある. cap5の中では,xは捕捉可能だ. しかし次の場合では,

(defmacro safe2 (expr)
  `(let ((x ,expr))
         (cons x 1)))

xは捕捉可能ではない. exprに渡された引数が評価される時点では,xの新たな束縛は可視でないからだ. 心配する必要があるのは骨格内の変数の束縛だけだということも気を付けて欲しい. 下のマクロでは,

(defmacro safe3 (var &body body)
  `(let ((,var ...))
         ,@body))

どのシンボルにも不注意による捕捉の危険はない (第1引数は束縛されるものと分かっているとして).

捕捉可能なシンボルを同定する規則を新しく手にした所で,forの元の定義を見てみよう.

(defmacro for ((var start stop) &body body)          ; 誤り
  `(do ((,var ,start (1+ ,var))
             (limit ,stop))
            ((> ,var limit))
         ,@body))

このforの定義は,2つの点で捕捉を生み出し得ることが分かる. まず元の例のようにlimitforの第1引数として渡され得る点だ.

(for (limit 1 5)
  (princ limit))

しかしlimitがループ本体内に現れたときも同じ位危険だ.

(let ((limit 0))
  (for (x 1 10)
        (incf limit x))
  limit)

forをこのように使う人は自分のlimitの束縛がループ内で1ずつ増やされ, 式全体も55を返すものと思っているだろう. しかし実際には,展開形の骨格に生成されたlimitの束縛だけが1ずつ増やされる.

(do ((x 1 (1+ x))
     (limit 10))
  ((> x limit))
  (incf limit x))

そしてそれが反復を制御する役目を持っているので,反復は終了さえしない.

この章で提示された規則は,あくまでも目安として意図されているという留保の元で使って欲しい. それらは形式に則って述べられてさえいないし,ましてや形式上正しくもない. 捕捉が問題になるのは元々の意図によることなので,定義は曖昧だ. 例えば下のような式では,

(let ((x 1)) (list x))

(list x)が評価されたときにxが新しい変数を参照することは,エラーとは見なさない. letにはそのような動作が期待されている. 変数捕捉を判別する規則もやはり厳密ではない. 上の3個のテストを満たしながら,なお意図しない変数捕捉を引き起こし得るようなマクロが書ける. 例えば下のようなマクロだ.

(defmacro pathological (&body body)                 ; 誤り
  (let* ((syms (remove-if (complement #'symbolp)
                          (flatten body)))
         (var (nth (random (length syms))
                   syms)))
    `(let ((,var 99))
       ,@body)))

このマクロが呼ばれると, 本体内の式はprognの中にあるかのように評価される ---しかし本体内のランダムなどれか1個の変数が違った値を持つかもしれない. これは明らかに変数捕捉だが,その変数は骨格内にはないので,上の規則を満たしている. しかし実践の場では,上の規則はほぼ必ず機能するだろう. 上の例のようなマクロを書きたいときは(あったとしても)めったにない.

適切な名前によって捕捉を避ける

第1, 2節では変数捕捉の実例を2種類に分けた: 引数の捕捉, これは引数内で使われたシンボルがマクロの骨格に生成された束縛に捕まるものだ. そしてフリー変数の捕捉, これはマクロの展開形内のフリーなシンボルが, マクロが展開された場所で効力を持つ束縛に捕捉されるものだ. 普通,後者は単にグローバル変数に区別の付くような名前を与えることで解決される. Common Lispのグローバル変数には,先頭と末尾にアスタリスクが付く名前を付けるのが伝統だ. 例えばカレント・パッケージを定義する変数は*package*と名付けられる. (このような名前は,普通の変数でないことを強調するために 「スター・パッケージ・スター」と発音されることがある.)

だからただのwではなく, *warnings*等の名前の変数に警告を蓄えるようにすることは, 完全にgripeの作者の責任だった. sample-ratioの作者が*warnings*を仮引数の名前に使っていたら, 現れるバグというバグはみな自分の責任だろう. しかし仮引数をwという名前にしても安全だろうと考えた点は責められることではない.

優先評価によって捕捉を避ける

危険のある引数をマクロ展開によって作られる束縛よりも外で評価することだけで, 引数の捕捉が回避できることがある. 一番単純なものはマクロを式letから始めることで回避できる. 第\ref{fig:AvoidCaptureWithLet}図にはマクロbeforeの2通りの定義を示したが, これは2個のオブジェクトと1個のシーケンスを引数に取り, そのシーケンス内で1個目のオブジェクトが2個目より前に現れるときに真を返すものだ \footnote{このマクロは例として使われているに過ぎない. 本当はマクロとして実装すべきでもないし,こんな非効率的なアルゴリズムを使うべきでもない. 適切な定義はfangページを参照.}.

捕捉を起こしやすい:
(defmacro before (x y seq)
  `(let ((seq ,seq))
     (< (position ,x seq)
       (position ,y seq))))
正しいヴァージョン
(defmacro before (x y seq)
   `(let ((xval ,x) (yval ,y) (seq ,seq))
      (< (position xval seq)
        (position yval seq))))
\caption{letによる変数捕捉の回避方法.} \label{fig:AvoidCaptureWithLet}

1個目の定義は不適切だ. 最初のletseqに渡された式が確かに1回だけ評価されるようにしているが、 これでは次のような問題を防ぐのに不十分だ。

> (before (progn (setq seq '(b a)) 'a)
          'b
          '(a b))
NIL

これは結局「(a b)の中でabより前にあるだろうか?」と尋ねているわけだ. 適切に作られたbeforeならば真を返すところだ。 マクロ展開を見ると実際に何が起きるかが分かる. <の第1引数の評価が第2引数内でも使われるリストを並べ替えてしまっている.

(let ((seq '(a b)))
  (< (position (progn (setq seq '(b a)) 'a)
               seq)
     (position 'b seq)))

この問題を回避するには,引数を全て1個の大きなletで最初に評価してしまえば十分だ. そのため第\ref{fig:AvoidCaptureWithLet}図内の2番目の定義には捕捉の危険はない.

残念なことに,letを使う技が通用する状況は多くない.

  1. 捕捉の危険のある引数はきっかり1回だけ評価されるべきで,
  2. マクロ骨格に生成された束縛のスコープ内ではどの引数も評価の必要はない,

というマクロでしか使えない. この制限は多数のマクロを切り捨ててしまう. 前に提案されたマクロforはどちらの条件も満たさない. しかしこの手法の変形を使うことで,forのようなマクロから捕捉の危険を取り除くことができる. ローカルに生成されたどの束縛よりも外側のλ式の中に実行本体となる式を包み込んでしまうのだ.

反復用マクロを含む幾つかのマクロは, マクロ呼出しに現れる式が新しく生成された束縛の中で評価されるような式を生成する. 例えばforの定義内では, 繰り返しの本体はマクロの作ったdoの中で評価されなければならない. そのため,繰り返し本体内に出て来る変数はdoに生成された束縛による捕捉の危険がある.

繰り返し本体内の変数をそのような捕捉から保護するためには, 本体をクロージャで包み,さらに繰り返しを行う際に, 式そのものを挿入せずにクロージャをfuncallで呼べばよい.

第\ref{fig:AvoidCaptureWithClosure}図にはこの技法を用いたヴァージョンのforを示した. クロージャはforの展開形の中で最初に作られるので, 繰り返し本体内に現れるフリーシンボルは全てマクロを呼んでいる環境内の変数を参照する. これならdoはクロージャの仮引数を通じてその本体と関わり合うことになる. 全てのクロージャは現在が繰り返しの何回目かをdoから伝えられる必要があるので, ただ1個の仮引数(マクロ呼び出しでインデックスに指定したシンボル)を持つ.

捕捉を起こしやすい:
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
     ((> ,var limit))
     ,@body))
正しいヴァージョン:
(defmacro for ((var start stop) &body body)
   `(do ((b \#'(lambda (,var) ,@body))
         (count ,start (1+ count))
         (limit ,stop))
        ((> count limit))
      (funcall b count)))
\caption{クロージャによる変数捕捉の回避方法.} \label{fig:AvoidCaptureWithClosure}

式をλ式で包む技法は普遍的な対処法ではない. これはコード本体の保護には使えるが,例えば(最初の不適切なforのように) 同じ変数が同じletdoに2回束縛される危険があるような場合には, クロージャは何の役にも立たない. 幸運なことにこの場合は,本体をクロージャ内に包むようにforを書き直したことで, doが引数varのために束縛を生成する必要がなくなっている. 元のforの引数varはクロージャの仮引数になっており, do内では実際のシンボルcountで置き換えてもよい. よって第9.3節のテストでも分かるように, forの新しい定義は変数捕捉の危険を完全に克服している.

クロージャを使うことの短所は,やや非効率的になるかも知れないことだ. 関数呼び出しを1回余計に増やしたことになったかも知れない. 事に依るともっと不都合なことに, コンパイラがクロージャにダイナミックエクステントを与えていなければ, そのためのスペースは実行時にヒープ領域に割り当てる必要が出るかもしれない.

Gensymによって捕捉を避ける

マクロの変数捕捉を避けるには,確実な方法が1つある. 捕捉され得るシンボルをgensymで置き換えてしまうのだ. forの元のヴァージョンでは, 問題は2個のシンボルが不注意から同じ名前を持ってしまったときに起きた. マクロ骨格が呼出側コードでも使われている名前を含むという可能性を回避したいなら, マクロ定義内では変な名前のシンボルだけを使うことで対処が望めるかも知れない:

(defmacro for ((var start stop) &body body)              ; 誤り
  `(do ((,var ,start (1+ ,var))
        (xsf2jsh ,stop))
     ((> ,var xsf2jsh))
     ,@body))

しかしこれは解決策とはとても言えない. これはバグを取り除いたのではなく,表面化しにくいようにしただけだ. それも大して表面化しにくいわけではない ---同じマクロを入れ子にして使ったときに起きる衝突がやはり想定できる.

シンボルが一意的であることを保証する方法が必要だ. Common Lispの関数gensymは,まさにこのために存在する. この関数はgensymと呼ばれるシンボルを返すが, これはコードに打ち込まれたりプログラムに生成されたどのシンボルとも eqではないことが保証されている.

Lispシステムはどうやってそれを保証するのだろうか? Common Lispの全てのパッケージは,その中で認識されている全てのシンボルのリストを保持している. (パッケージへの導入に付いては,chuanページを参照.) そのリストに載っているシンボルはパッケージにインターンされていると言われる. gensymを呼び出す度に,一意でインターンされていないシンボルが返される. そしてreadに読み取られるシンボルは全てインターンされるので, gensymと等しいものを打ち込むことはできない. そのため,次のように始まる式は,

   (eq (gensym) ...

何を続けても真を返すようにすることはできない.

gensymにシンボルを生成させることは, 奇妙な名前のシンボルを選ぶ手法を一歩進めたようなものだ ---gensymは,電話帳を探しても載っていないような名前のシンボルを返す. Lispがgensymを表示しなければならないときは,次のようにする.

> (gensym)
#:G47

表示されるものはLispにとって「名無の権兵衛」程のものに過ぎない. これは任意の名前で,名前が意味を持つことがないように作られたものだ. そしてこの表示について余計な想像を一切引き起こさないように, gensymsはシャープ・コロンに続いて表示される. これは特殊なリードマクロで, 表示されたgensymを再び読み込もうとしたときエラーを起こすためだけに存在している.

捕捉を起こしやすい:
(defmacro for ((var start stop) &body body)
  `(do ((,var ,start (1+ ,var))
        (limit ,stop))
     ((> ,var limit))
     ,@body))
正しいヴァージョン:
(defmacro for ((var start stop) &body body)
  (let ((gstop (gensym)))
    `(do ((,var ,start (1+ ,var))
          (,gstop ,stop))
         ((> ,var ,gstop))
       ,@body)))
\caption{Gensymによる変数捕捉の回避方法.} \label{fig:AvoidCaptureWithGensym}

CLtL2に従うCommon Lispでは, gensymの印字表現に現れる数は*gensym-counter*から来ている. これは常に整数に束縛されているグローバル変数だ. このカウンタを手動で設定することで2個のgensymを同じように表示させることができる.

> (setq x (gensym))
#:G48
> (setq *gensym-counter* 48 y (gensym))
#:G48
>(eqxy)
NIL

しかしこれらは同一ではない.

第\ref{fig:AvoidCaptureWithGensym}図には,gensymを使ったforの正しい定義を載せた. マクロに渡された式の中のシンボルと衝突を起こしていたlimitはもうない. それはマクロ展開の時点で生成されたシンボルに置き換わってしまう. マクロが展開される度, limitの場所には展開時に生成された一意な名前のシンボルが代わりに置かれる.

forの正しい定義は一発で書き上げるには複雑過ぎる. 完成品のコードは,完成した数学定理のように, しばしば多くの試行錯誤を覆い隠している. だからあるマクロを何段階にも分けて書かなくてはならなくても心配しないことだ. forのようなマクロを書き始めるには, 最初のヴァージョンは変数捕捉について考えずに書き, そうしたら前に戻って変数捕捉に関わるシンボルをgensymで置き換えるのがいいかも知れない.

パッケージによって捕捉を避ける

ある程度までは,マクロを独自のパッケージに入れることで変数捕捉を避けることができる. パッケージmacrosを作ってその中でforを定義すれば,最初に挙げた定義

(defmacro for ((var start stop) &body body)
     `(do ((,var ,start (1+ ,var))
             (limit ,stop))
            ((> ,var limit))
         ,@body))

を使っても他のパッケージからは安全に呼び出せる. forを別のパッケージ(例えばmycode)から呼ぶと, 第1引数にlimitを使っても,それはmycode::limitになる ---これはマクロ骨格に現れるmacros::limitとは別物だ.

しかしパッケージは変数捕捉の問題の一般的な解決策にはならない. 第1には,マクロはあるプログラムに統合された一部分であって, それらを独自のパッケージ内に分けておかなければならないのでは不便だ. 第2には,パッケージmacros内の別のコードに対する保護には全くなっていない.

異なる名前空間での捕捉

これまでの章ではあたかも捕捉が変数のみに悪さをする問題かのように扱ってきた. 確かにほとんどの捕捉は変数捕捉だが, Common Lispの他の名前空間においても同様に問題が起き得る.

関数もローカルな束縛を持つことができ,そして関数の束縛も同様に不用意な捕捉を引き起こす可能性がある. 例:

> (defun fn (x) (+ x 1))
FN
> (defmacro mac (x) `(fn ,x))
MAC
> (mac 10)
11
> (labels ((fn (y) (- y 1)))
    (mac 10))
9

捕捉を見つける規則から予測できるように, macの骨格内でフリーなまま使われているfnには捕捉の危険がある. fnがローカルに再束縛されると,macの返す値は普通とは違ったものになる.

これにはどう対処すればよいだろうか? 捕捉の危険のあるシンボルが組込みの関数やマクロの名前ならば,何もしないでおくのが理に適っている. CLtL2によれば(p. 260), 組込みのものの名前がローカル関数またはマクロに束縛されたとき,「結果は定義されない」. だから書いたマクロの動作の問題ではない. 組込み関数を再束縛したら,自分のマクロだけでなく様々な問題に悩まされることになるだろう.

そうでないとき,変数名を保護するのと同じ方法で関数名をマクロ引数の捕捉から保護できる. マクロ骨格によってローカルに定義されたどのような関数にも,gensymを関数名に使うことだ. この場合,フリーなシンボルの捕捉を避けることは少し難しくなる. 変数をフリーシンボル捕捉から保護するには,はっきり区別できるグローバル変数用の名前を付ければよかった: 例えばwでなく*warnings*を使えばよい. しかしこの解決策は関数には有効ではない. グローバル関数を区別するための命名法には慣習が無いからだ ---何しろ大部分の関数がグローバルなのだから. マクロが,必要な関数がローカルに再定義されているかもしれない環境内で呼ばれることが心配なら, 最良の解決策は,おそらくコードを独立したパッケージに入れることだ.

ブロック名,すなわちgothrowで使われるタグからもやはり捕捉が起こり得る. マクロにそのような名前のシンボルが必要なら,our-doの定義と同様にgensymを使うべきだ.

またdo等のオペレータは暗黙のうちにnilという名のブロックに囲まれることも忘れてはいけない. そのためdoの中のreturnreturn-from nilは, doの中の式ではなくdoそのものから制御を戻すことになる.

> (block nil
         (list 'a
               (do ((x 1 (1+ x)))
                 (nil)
                 (if (> x 5)
                   (return-from nil x)
                   (princ x)))))
12345
(A 6)

donilという名前のブロックを作っていなかったら, この例は(A 6)でなくただの6を返していたはずだ.

doの暗黙のブロックは問題とは言えない. doはそのように振る舞うことになっているからだ. しかし展開形がdoを含むようなマクロを書いたときは, それらがブロック名nilを捕捉してしまうことは覚えておくべきだ. forのようなマクロでは, returnreturn-from nilは,外側のブロックではなくforから制御を戻すことになる.

変数捕捉にこだわる理由

これまで示した例には極めて病的と言っていいものもあった. それらを見て,こう言いたくなった人もいるかもしれない. 「変数捕捉なんて滅多に起きるもんじゃない,なんで気に病まなきゃいけないんだ?」 この問には2通りの答え方がある. 1つ目は別の質問を返すことだ: バグのないプログラムが書けるのに,どうして少々のバグを持ったプログラムを書くのですか?

長い答え方は,実際の応用では 書いたコードの用途について予測を立てるのは危険だと指摘することだ. 全てのLispプログラムは,現在「オープン・アーキテクチャ」と呼ばれる構造を持つ. 他人の使うコードを書いているのなら, その人達は予測もつかない方法でそれを使うかも知れない. また,心配しなければならないのは他人だけではない. プログラムもプログラムを書くのだ.

(before (progn (setq seq '(b a)) 'a)
        'b
        '(a b))

このようなコードを書く人間はいないかもしれないが, プログラムに生成されたコードはしばしばこのような形をしている. 個々のマクロが単純で理に適った外見の展開形を生成するときでさえ, マクロ呼び出しにマクロ呼び出しを渡したりすれば, 途端に展開形はいかにも人が書いたものには見えない大きなプログラムになり得る. そのような場合は,マクロが誤って展開されてしまう状況に対しては, それがどんなにありそうもないものでも,防御策を取っておいて損はない.

最後に言いたいのは,捕捉を避けるのはそれ程難しくはないということだ. 少し経てば習慣になってしまう. Common Lispの古典的なdefmacroは料理人の包丁のようなものだ. エレガントな道具で危険にも思えるが,達人は確信を持って使っている.


←: いつマクロを使うべきか     ↑: On Lisp     →: マクロのその他の落し穴

Copyright (c) 2003-2011 野田 開     NODA Kai <nodakai@gmail.com>