マクロ

Lispのマクロ機能を使うと,コードの変換によってオペレータを実装することができる. マクロ定義とは実質的にLispコードを生成する関数だ ---つまりプログラムを書くプログラムだ. この一歩から始めて,大きな可能性と予期せぬ危険に足を踏み入れていく. 第7章から第10章はマクロの基礎講座だ. この章ではマクロがどのように動作するのかを説明し, マクロを書いたり動作を確かめるときのテクニックを示し, マクロを書くスタイルについての話題に進む.

マクロはどのように動作するか

マクロは呼び出せるし値を返せるので,関数と一緒くたにされがちだ. マクロ定義は関数定義に似ていることもあるし, 実際はマクロであるdoを普段「組み込み関数」と呼ぶ人も多い. しかしこの喩えを突き詰めすぎると混乱の元になる. マクロの動作は普通の関数とは違っている. マクロはどのように,そしてなぜ違うのかを知ることは,マクロを正しく使うための鍵だ. 関数は結果を生むが,マクロはを生む ---そしてこの式が評価されると結果を生む.

習い始めるのに一番良い方法は,実例に当たることだ. ここでは引数をnilに設定するマクロnil!を書きたいとしよう. (nil! x)としたとき(setq x nil)と同じ効果が欲しいのだ. それにはnil!を,ある形を持った式を別の形の式に変えるマクロとして定義する.

> (defmacro nil! (var)
    (list 'setq var nil))
NIL!

人間語に直せば,この定義はLispに 「(nil! var)の形の式を見たら, 必ず評価前に(setq var nil)の形に変えること.」 と命じている.

マクロの生成した式は元のマクロ呼び出しの場所で評価される. マクロ呼び出しとは第1要素がマクロの名前であるリストだ. マクロ呼び出し(nil! x)をトップレベルに打ち込んだら何が起きるだろうか? Lispはnil!がマクロの名前であることに気付き,

  1. 与えた定義の指定した式を生成し,
  2. その式を元のマクロ呼び出しの場所で評価する.

新しい式を生成する工程はマクロ展開と呼ばれる. Lispはnil!の定義を探すが, そこにはマクロ呼び出しを置き換える式の生成法が載っている. nil!の定義が, マクロ呼び出しの引数として与えられた式に対して関数のように適用される. それは3個の要素(setq,マクロに引数として与えられた式,nil) から成るリストを返す. この場合nil!の引数はxだから,マクロ展開の結果は(setq x nil)だ.

マクロ展開の後,第2工程すなわち評価が行われる. Lispは展開結果(setq x nil)を,始めの場所に直接書いてあったかのように評価する. 必ずしもトップレベルのときのように評価が展開直後に行われる訳ではない. 関数定義内のマクロ呼び出しは関数がコンパイルされたときに展開されるが, 展開形(またはそれに基づくオブジェクト・コード)は その関数が呼び出されるまで評価されない.

マクロに関して出会う問題の多くは, マクロ展開と評価をきちんと区別することで避けられる. マクロを書くときは,展開時と評価時に, それぞれどのような動作をするのかしっかり理解すること. なぜなら二つの工程は一般的に別の種類のオブジェクトに対して作用するからだ: マクロ展開は式を扱い,評価はその値を扱う.

マクロ展開はnil!の場合より複雑になることがある. nil!の展開形は組み込み特殊オペレータの呼び出しだったが, 展開形が別のマクロ呼び出しになることもある. ちょうどロシア人形の中に別の人形が入っているようなものだ. そのようなとき,マクロ展開はマクロ呼び出しでない式に到達するまで続く. この過程が最終的に終わるまでには,任意の長さの段階を要する.

多くのプログラミング言語は何らかの種類のマクロを備えているが, Lispのマクロは並外れて強力だ. Lispコードのファイルがコンパイルされるとき, パーサがソースを読んで出力をコンパイラに送る. 大事なのは次だ. パーサの出力はLispのオブジェクトのリストから成る. マクロを使えば, プログラムがパーサとコンパイラの間の中間形式のときに操作することができる. 必要ならばその操作でLispの拡張も行える. 展開形を生み出すマクロはLispの力の全てを握っている. それはマクロが生まれ持った性質だ. 実際,マクロはLispの関数だ ---たまたま式を返すだけのことだ. nil!の定義ではlistが使われただけだったが, 別のマクロでは展開時に大きなサブルーチンを呼び出すこともある.

コンパイラが読む情報を書き換えられるというのは コンパイラを書き換えられるのとほとんど同じだ. 既存のデータ構造を変形することで定義できるようなデータ構造なら何でもLispに追加できる.

逆クォート

逆クォート(バッククォート,backquote)は特別なクォートで, Lispの式の雛形を作るのに使える. それが一番よく使われるのはマクロ定義の中だ.

逆クォート`は,普通のクォート'を逆さにした形なのでそう呼ばれる. 逆クォートが式の前に付いているだけのときは,働きはクォートと変わらない:

`(a b c)'(a b c)と等価である.

逆クォートが便利なのは, カンマ,やカンマ・アット,@と組み合わせて使うときだ. 逆クォートがリストの雛形になるとき,カンマはその中に値を挿入する. 逆クォート付きリストは,各要素にクォートを付けてlistを呼ぶのと等価だ. つまり

`(a b c)(list 'a 'b 'c)と等価である.

逆クォートのスコープ内では,カンマはLispに「評価の保護を止めろ」と命じる. カンマがリストのある要素の前に付くと, そこに仮想的に付けられているクォートの効果を打ち消す. だから

`(a ,b c ,d)(list 'a b 'c d)と等価である.

シンボルbの代わりにその値が挿入されたリストが作られる. どれ程深く入れ子になったリスト内でもカンマは機能する:

> (setq a 1 b 2 c 3)
3
> `(a ,b c)
(A 2 C)
> `(a (,b c))
(A (2 C))

またカンマの前にクォートが付いてもよいし,クォート付きの部分リスト内で使ってもよい:

> `(a b ,c (',(+ a b c)) (+ a b) 'c '((,a ,b)))
(A B 3 ('6) (+ A B) 'C '((1 2)))

1個のカンマは1個の逆クォートの効果を打ち消すのだから, カンマは逆クォートと対応が取れなければならない. ある特定のオペレータがカンマの前に付くか,カンマを含む式の前に付いているとき, 「カンマはそのオペレータに囲まれている」と言うことにしよう. 例えば`(,a ,(b `,c)))では,最後のカンマはカンマ1個と逆クォート2個に囲まれている. 一般的に言うと:$n$個のカンマに囲まれたカンマは 最低$n+1$個の逆クォートに囲まれていなければならない. それから明らかに分かるのは, カンマは逆クォートの付いた式の外側にあってはならないということだ. 逆クォートとカンマは,上の規則に従う限り入れ子になってもよい. 次の式はトップレベルに打ち込んだらどれもエラーになる:

,x   `(a ,,b c)   `(a ,(b ,c) d)   `(,,`a)

逆クォートの中の逆クォートは,マクロを定義するマクロ位でしか使われない. どちらの点についても第16章で議論する.

逆クォートは大抵リストを作るのに使われる \footnote{逆クォートはベクタを作るのにも使えるが, マクロ定義内では滅多にそういうことはしない.}. 逆クォートの作るリストはlistと普通のクォートを使って作ることもできる. 逆クォートを使うことの利点は, 逆クォート付きの式はそれの生成する式と似ているので式を読み易くできることだ. 前章でnil!を次のように定義した:

(defmacro nil! (var)
  (list 'setq var nil))

逆クォートを使うと,同じマクロが次のように定義できる:

(defmacro nil! (var)
  `(setq ,var nil))

この場合では違いはあまりないが, マクロ定義が長くなれば逆クォートを使う重要性は増す. 第\ref{fig:Backquote}には, 数の符号についてif分岐を行うマクロnifの定義の候補を二つ示した \footnote{このマクロ定義はgensymの使用を避けるために変なやり方をしている. よい定義方法はzipページで示す.}.

第1引数は評価されると数にならなければならない. そしてその数か正か0か負かに応じてそれぞれ第2,3,4引数が評価される:

> (mapcar #'(lambda (x)
              (nif x 'p 'z 'n))
          '(0 2.5 -8))
(Z P N)
逆クォートを使う:
(defmacro nif (expr pos zero neg)
  `(case (truncate (signum ,expr))
     (1 ,pos)
     (0 ,zero)
     (-1 ,neg)))
逆クォートを使わない:
(defmacro nif (expr pos zero neg)
  (list 'case
        (list 'truncate (list 'signum expr))
        (list 1 pos)
        (list 0 zero)
        (list -1 neg)))
\caption{マクロ定義に逆クォートを使った例と使わない例.} \label{fig:Backquote}

第\ref{fig:Backquote}図の二つのマクロ定義は同じマクロを定義している. ただ1番目は逆クォートを使っているが, 2番目ではlistを陽に呼ぶことで展開形を作っている. 最初の定義からは,例えば(nif x 'p 'z 'n)

(case (truncate (signum x))
  (1 'p)
  (0 'z)
  (-1 'n))

に展開されるのはすぐ分かる. マクロ定義本体の形が,生成される展開形にそっくりだからだ. 逆クォートのない2番目の定義を理解するには, 展開の過程を頭の中で追っていかなければならない.

カンマ・アット,@はカンマの変種で,機能はカンマと同じだが違いが1点ある: 次に続く式の値をカンマのようにそのまま挿入するのでなく, カンマ・アットは切り張り操作を行う. つまり1番外側の括弧を取り除いて挿入する:

> (setq b '(1 2 3))
(1 2 3)
> `(a ,b c)
(A (1 2 3) C)
> `(a ,@b c)
(A 1 2 3 C)

カンマはリスト(1 2 3)bの所に挿入するが, カンマ・アットはそこにリストの要素を挿入する. カンマ・アットを使うには幾つか制限がある:

  1. 引数に切り張り操作を行うので,カンマ・アットは連続構造内になければならない. `,@b等とするのは誤りだ. bの値を張り付ける先がないからだ.
  2. 切り張りされるオブジェクトは,最後に来ない限りリストでなければならない. 式`(a ,@1)が評価されると(a . 1)となるが, `(a ,@1 b)等として, アトムをリストの中に切り張りしようとするとエラーになる.

カンマ・アットが使われるのは, 不定個の引数を取り,やはり不定個の引数を取る関数やマクロに渡すマクロの中が多い. 普通この状況は暗黙のブロックを実装するときに現れる. Common Lispにはblocktagbodyprogn等, コードをブロックにまとめるオペレータが幾つかある. これらのオペレータはソース・コード内には滅多に出て来ない. それらは暗黙に使われる ---つまり,マクロに隠されている.

暗黙のブロックは式を実行本体として持てる組み込みマクロでは必ず使われている. 例えばletcondには暗黙のprognが使われている. そんな中で最も単純な組み込みマクロはwhenだろう:

(when (eligible obj)
  (do-this)
  (do-that)
  obj)

(eligible obj)が真を返すとき残りの式が評価され, whenが式として返す値はそのうち最後の式の値だ. コンマ・アットを使った例として,ここにwhenの定義方法の一例を示す:

(defmacro our-when (test &body body)
  `(if ,test
       (progn
         ,@body)))

この定義は&body仮引数を使っているが, これは表示整形(pretty-printing)の効果を除いて&restと同じで, 任意個の引数が取れるようにしている. またそれらを式prognの中に切り張りするためにカンマ・アットを使っている. 上の呼び出しでの展開形では,本体の3個の式は1個のprognの中に現れる:

(if (eligible obj)
    (progn (do-this)
           (do-that)
           obj))

繰り返しを行うマクロの大半は似た方法で引数を切り張りする.

カンマ・アットの効果は逆クォートを使わなくても実現できる. 例えば式`(a ,@b c)(cons 'a (append b (list 'c)))と等価だ. カンマ・アットの存在理由は上のように式を生成する式を読み易くすることだけだ.

マクロ定義は(普通)リストを生成する. マクロ展開は関数リストを使っても作れるが, 逆クォートを使ったリストの雛形は仕事を随分楽にしてくれる. defmacroと逆クォートで定義したマクロは, 表面的にはdefunで定義した関数に似ている. この類似性にミスリードされない限り, 逆クォートによってマクロを書くのも読むのも楽になる.

逆クォートはマクロ定義の中で非常によく使われるので, defmacroの一部だと思っている人もいる. 逆クォートについて忘れてはいけないことの最後は, それがマクロ内での役割とは別に自分自身の生き方があるということだ. 連続構造を生成する必要のあるときには,いつでも逆クォートが使える:

(defun greet (name)
  `(hello ,name))

単純なマクロの定義

プログラミングの学習では, できるだけ早く実験することが一番の方法であることが多い. 理論的に完全に理解するのは後でもいい. それに従って,この章ではマクロをすぐに書き始める方法を提示する. その方法が使える範囲は広くはないが,可能なところでは機械的に適用できる. (もうマクロを書いた経験があるのなら,この章は飛ばしてもよい.)

例としてCommon Lispの組み込み関数memberの変種を書く方法について考える. 普通memberは2個のオブジェクトが等しいかどうか調べるのにeqlを使う. eqを使って所属関係を調べたいときには,わざわざ指定しなければならない:

(member x choices :test #'eq)

これを何度も繰り返すことがあるなら, 必ずeqを使うmemberの変種を書いてもいいだろう. 幾つかの昔のLisp方言にはそんな関数があり,memqと呼ばれていた:

(memq x choices)

普通memqはインライン関数として定義されるが, マクロの例を示すため,ここではマクロとして復活させる.

\caption{memqを書くのに使われた線図.} \label{fig:DiagramMemq}

さて方法とは: まず定義したいマクロの典型的な呼び出し方から始める. メモ用紙にそれを書き,その下にそれが展開されてできる筈の式を書く. 第\ref{fig:DiagramMemq}図にそのような2個の式の例を示した. マクロ呼び出しに従い,これから書くマクロの仮引数リストを作る. 引数それぞれについて適当な仮引数名を考える. この場合引数が2個あるので仮引数を2個使う. それらをobjlstと呼ぼう:

(defmacro memq (obj lst)

そして先程書いた2個の式に戻る. マクロ呼び出しの中の引数それぞれから, 下の展開形の中で使われている場所まで線を引く. 第\ref{fig:DiagramMemq}図には平行な2本の線が引いてある. マクロ本体を書くには展開形に注意を向けなければいけない. まず逆クォートから本体を書き始める. 次に展開形を式毎に区切って読んでいく. マクロ呼び出しの引数の一部でない括弧を見つけたら,マクロ定義内にも括弧を書く. すると逆クォートの次には左括弧が来る. そうしたら展開形の式それぞれについて

  1. マクロ呼び出しと線でつながってなければ,その式そのものを書く.
  2. マクロ呼び出しの引数の一つとつながっていれば, マクロの仮引数リスト内で対応するシンボルを書き,その前にカンマを付ける.

第1要素memberはどこにもつながっていないので, memberそのものを使う:

(defmacro memq (obj lst)
  `(member

しかしxからは元の式の第1引数に線が引いてあるので, マクロ本体の中にカンマを付けた第1引数を置く:

(defmacro memq (obj lst)
  `(member ,obj

この要領で続けていくと次のようなマクロ定義が完成する:

(defmacro memq (obj lst)
  `(member ,obj ,lst :test #'eq))

これまでのところ,決まった数の引数を取るマクロだけを書いてきた. さて今書きたいのはwhileだとしよう. これはテスト式と本体となる幾らかの式を引数に取り, テスト式が真を返す限りその式を繰り返し実行する. 第\ref{fig:DiagramWhile}図に猫の振る舞いを描写したwhileループの例を示した.

\caption{whileを書くのに使われた線図.} \label{fig:DiagramWhile}

そのようなマクロを書くには,先程の方法を少し修正する必要がある. まず前と同じようにマクロ呼び出しの例を書くことから始める. それからマクロの仮引数リストを書くのだが, 任意個の引数を取りたいところでは &restまたは&bodyの次に仮引数を置く:

(defmacro while (test &body body)

さて展開される筈の形をマクロ呼び出しの下に書き, 前と同じようにマクロ呼び出しの引数を展開形内の場所までつなぐ線を引こう. しかし幾つかの引数が 単一の&restまたは&body仮引数として詰め込まれるときは, それらを塊として扱い,引く線はそれら全体に対して1本にする. 結果の線図は第\ref{fig:DiagramWhile}図に示した.

そしてマクロ定義の本体を書くには展開形を参考に前と同じことをするが, 先程の2個に加えもう1個の規則が必要だ:

  1. 展開形内の幾つかの式からマクロ呼び出し内の幾つかの引数へつながる線があれば, 対応する&rest(または&body)仮引数を書き, その前にカンマ・アットを置く.

結局マクロ定義は以下のようになる:

(defmacro while (test &body body)
  `(do ()
     ((not ,test))
     ,@body))

式を本体として持つマクロを書くには,幾つかの仮引数が漏斗として働く必要がある. ここではマクロ呼び出し内の複数の引数が本体にまとめられ, 本体が展開形の中に切り張りされるときに再び分解されている.

この章で説明した手法を使えば,最も単純なマクロ ---仮引数を並び替えるだけのもの--- を書くことができる. しかしマクロの力はそんなものではない. 第7.7節では,展開形がただの逆クォート付きリストでは表現できないような例を示す. そういうマクロを展開するにはマクロ自身がプログラムでなければならない.

マクロ展開の確認

マクロを書くのが終わったら,どうやってその動作を確かめよう? memqのようなマクロは単純だから,ちょっと見ればその動作が分かる. 更に複雑なマクロを書くときは,それが正しく展開されるのか確認できなければならない.

> (defmacro while (test &body body)
    `(do ()
       ((not ,test))
       ,@body))
WHILE
> (pprint (macroexpand '(while (able) (laugh))))
(BLOCK NIL
       (LET NIL
            (TAGBODY
              #:G61
              (IF (NOT (ABLE)) (RETURN NIL))
              (LAUGH)
              (GO #:G61))))
T
> (pprint (macroexpand-1 '(while (able) (laugh))))
(DO NIL
    ((NOT (ABLE)))
    (LAUGH))
T
\caption{1個のマクロとその2段階の展開.} \label{fig:Macroexpand}

第\ref{fig:Macroexpand}図には,1個のマクロ定義とその2通りの展開形を示した. 組み込み関数macroexpandは指揮を引数に取り,それをマクロ展開したものを返す. マクロ呼び出しをmacroexpandに渡すと, それが評価前に最終的にどのように展開されるのかが分かる. しかし完全な展開形はマクロの確認では必ずしも望ましいものではない. 問題のマクロが別のマクロを使っていると展開が進みすぎてしまうので, 完全な展開形は読みづらいことがある.

第\ref{fig:Macroexpand}図の1番目の式からは, whileが意図した通りに展開されたかどうかは判断し辛い. 組み込みマクロのdoprogに展開され,さらにそれも展開されているからだ. ここで必要なのは1段階だけ展開した結果を見る方法だ. それが2番目の例に示した組み込み関数macroexpand-1の目的だ. macroexpand-1は1段階だけマクロを展開すると, それが依然としてマクロ呼び出しであっても動作を止める.

(defmacro mac (expr)
  `(pprint (macroexpand-1 ',expr)))
\caption{マクロ展開確認用のマクロ.} \label{fig:TestExpand}

マクロ呼び出しの展開形を見たいとき,いつも

(pprint (macroexpand-1 '(or x y)))

と打ち込むのは馬鹿らしい. 第\ref{fig:TestExpand}図の新しいマクロを使うなら,代わりにこうすればよい:

(mac (or x y))

デバッグを進めるときの定石は,関数は呼び出すこと,マクロは展開することだ. しかしマクロ呼び出しは2層に渡る命令が関わるので,誤りの起きる場所も2箇所ある. マクロの動作がおかしいときは,大抵は展開形を見るだけで間違いが見つかる. しかし,希に展開形には問題ないように見えて, 問題がどこで起きるのかを見るためにそれを評価したいときもある. 展開形にフリー変数が含まれるときは,先にそれらを設定したいこともあるだろう. 処理系によっては,展開形をコピーしてトップレベルに貼り付けたり, 展開形を選択してメニューからevalを選んで評価させることができる. どうしても問題が掴めないときは,適当な変数をmacroexpand-1の返すリストに設定し, それに対してevalを呼ぶこと:

> (setq exp (macroexpand-1 '(memq 'a '(a b c))))
(MEMBER (QUOTE A) (QUOTE (A B C)) :TEST (FUNCTION EQ))
> (eval exp)
(A B C)

最後に,マクロ展開は単なるデバッグの補助手段ではなく, マクロの書き方の勉強手段でもあることを言っておきたい. Common Lispには100以上の組み込みマクロがあり,中には大変複雑なものもある. そんなマクロの展開形を見ることで,それらがどう書かれたのかが分かることも多い.

引数リストの構造化代入

構造化代入(destructuring)とは,関数呼び出しによって行われる一種の代入 \footnote{構造化代入は大抵,代入を行うのでなく束縛を作るオペレータで見られる. しかし概念的には構造化代入は値の代入の一手法であって, 新しい変数だけでなく既存の変数に対しても機能する. つまり構造化代入型setqを書いて悪いことはない.}の一般化だ. 幾つかの引数を取る関数を定義したとき

(defun foo (x y z)
  (+ x y z))

その関数が呼ばれると

(foo 1 2 3)

関数の仮引数には順番に従って引数が代入される: xには1,yには2,そしてzには3という風に. 構造化代入とは,このような引数の場所に従って行われる代入が, 任意のリスト構造に対して(x y z)と同様に行われることだ.

Common Lispのマクロdestructuring-bind(CLtL2で新しく導入された)は, パターン,リストに評価される引数及び本体となる式を引数に取り, パターン内の仮引数をリスト内の対応する要素に束縛した状態で式を評価する:

> (destructuring-bind (x (y) . z) '(a (b) c d)
    (list x y z))
(A B (C D))

この新オペレータとその仲間が第18章の主題になる.

構造化代入はマクロの仮引数リストでも使える. Common Lispのdefmacroでは仮引数リストは任意のリスト構造であってよい. マクロ呼び出しが展開されたとき, マクロ呼び出しのcomponentsはマクロの仮引数に destructuring-bindと同様に代入される. 組み込みマクロdolistは仮引数リストの構造化代入を活用している. 次のようなマクロ呼び出しでは:

(dolist (x '(a b c))
  (print x))

展開されて生まれる関数は,第1引数として与えられたリストの中から x'(a b c)を取り出さなければならない. それはdolistに適切な仮引数リストを与えることで暗黙の中に実現された \footnote{後に導入するgensymの使用を避けるため, ここではおかしな方法で定義している.}:

(defmacro our-dolist ((var list &optional result) &body body)
  `(progn
     (mapc #'(lambda (,var) ,@body)
           ,list)
     (let ((,var nil))
       ,result)))

Common Lispでは,普通dolistのようなマクロは 実行本体でない引数をリスト内にまとめ込んでいる. dolistはそれが返す結果になる付加的な引数を取るので, 何にせよ1種類目の引数を独立したリストにまとめなれけばならない. しかし余分なリスト構造が必要でなくても, それによってdolistの呼び出しが読み易くなる. ここでwhenに似たマクロwhen-bindを定義したいとしよう. ただしこれは調べる式の返す値に何かの変数を束縛するものとする. このマクロは入れ子になった仮引数リストを使うと一番上手く実装できるだろう:

(defmacro when-bind ((var expr) &body body)
  `(let ((,var ,expr))
     (when ,var
       ,@body)))

これは次のように呼び出せばよく,

(when-bind (input (get-user-input))
           (process input))

こうしなくてもいい:

(let ((input (get-user-input)))
  (when input
    (process input)))

Used sparingly, 仮引数リストの構造化代入は明確なコードにつながる. 少なくとも, 2個以上の引数と本体となる式を取るwhen-binddolist等のマクロで使える.

マクロのモデル

マクロが何をするのかを形式的に説明しても,長く,ややこしくなるだけだろう. 経験を積んだLispプログラマの頭の中にもそんな説明がある訳ではない. defmacroの定義方法を想像することで,それが何をするのかを覚えた方が便利だ.

Lispにはそのような説明を使う長い伝統がある. 1962年初版のThe Lisp 1.5 Programmer's Manual\note{Lisp1.5}も, 参考のためにLispで書かれたevalの定義を載せている. defmacro自身がマクロなので,同じ扱いができる. 定義は第\ref{fig:SketchDefmacro}図に示した. そこではまだ扱っていない技法を幾つか使っているので, 参考にするのは後にしたいと思われる方もいるだろう.

(defmacro our-expander (name) `(get ,name 'expander))

(defmacro our-defmacro (name parms &body body)
  (let ((g (gensym)))
    `(progn
       (setf (our-expander ',name)
             #'(lambda (,g)
                 (block ,name
                        (destructuring-bind ,parms (cdr ,g)
                          ,@body))))
       ',name)))

(defun our-macroexpand-1 (expr)
  (if (and (consp expr) (our-expander (car expr)))
      (funcall (our-expander (car expr)) expr)
      expr))
\caption{defmacroの概形.} \label{fig:SketchDefmacro}

第\ref{fig:SketchDefmacro}図に示した定義は, マクロが何をするのかについてかなり正確な感覚を与えてくれるが, 飽くまでも概形なので不完全な所もある. これは&wholeキーワードを適切に扱えない. またdefmacroが第1引数にマクロ関数として実際に保持させるものは2引数の関数で, マクロ呼び出しと,それが呼び出されたレキシカル環境を引数に取る. しかしそれらの性質が使われるのは最高度に難解なマクロだけだ. マクロが第\ref{fig:SketchDefmacro}図のように実装されていると思っていれば, 間違うことはまず無いだろう. 例えばこの本の中で定義されたマクロはそれを使っても全て正しく機能する.

第\ref{fig:SketchDefmacro}図の定義が生み出す展開形の関数は, シャープクォート付きのλ式だ. つまりそれはクロージャである筈だ. マクロ定義内のどんなフリーシンボルも defmacroの呼び出された環境内の変数を参照できる. だから次のようにすることができる筈だ:

(let ((op 'setq))
  (defmacro our-setq (var val)
    (list op var val)))

CLtL2では,確かに可能だ. しかしCLtL1では,マクロ展開関数は空レキシカル環境で定義されていたので \footnote{この区別が問題になるマクロの例については,第pomページの注を参照.}, 古い処理系では上のour-setqは動作しないかもしれない.

プログラムとしてのマクロ

マクロ定義は必ずしも単なる逆クォート付きリストでなくてもよい. マクロはある種の式を別の形に変形する関数だ. その関数は結果を生成するためにlistを呼んでもいいが, 数百行のコードから成る副プログラムを丸ごと呼んでもいい.

第7.3節では,マクロを簡単に書く方法を示した. その方法によって,展開形がマクロ呼び出しの中と同じ式を含むマクロを書くことができる. しかし残念なことに,その条件を満たすのは一番単純な種類のマクロだけだ. 複雑なマクロの例として,組み込みマクロdoについて考えてみよう. doは引数を並び替えるだけのマクロとして定義することは不可能だ. その展開時には,マクロ呼び出しには決して出て来ない複雑な式を生成しなければならない.

マクロを書くときの更に一般的な手法とは, 使えるようにしたい式の種類と,それがどのように展開されるかについて考え, 使いたい式を展開形に変形するプログラムを書くことだ. マクロの例を手で展開してみて, ある形が変形されるときには何が起きているのかを見ると良い. 例に倣うことで,自分の求めるマクロには何が必要なのかを掴むことができる.

(do ((w 3)
     (x 1 (1+ x))
     (y 2 (1+ y))
     (z))
  ((> x 10) (princ z) y)
  (princ x)
  (princ y))

これは次のようなものに展開されて欲しい:

(prog ((w 3) (x 1) (y 2) (z nil))
      foo
      (if (> x 10)
          (return (progn (princ z) y)))
      (princ x)
      (princ y)
      (psetq x (1+ x) y (1+ y))
      (go foo))
\caption{doの望ましい展開方法.} \label{fig:DesiredExpansionDo}

第\ref{fig:DesiredExpansionDo}図に示したのは,doの使用例と, それが展開されて生まれる筈の式だ. 自分の手で展開してみるのはマクロがどのように働くべきかを明確にするのに良い方法だ. 例えば,展開形を手で書かないと, ローカル変数の更新にpsetqを使わなければならないことは明らかではないだろう.

組み込みマクロpsetq(``parallel setq''つまり「並列setq」) の動作はsetqと似ているが, 全ての(偶数番目の)引数がどの代入よりも前に評価される点が違う. 普通のsetqが2個より多い引数を取るとき, 第1引数の新しい値は第4引数の評価中に見えている:

> (let ((a 1))
    (setq a 2 b a)
    (list a b))
(2 2)

ここではaが最初に設定されたので,baの新しい値の2を得た. psetqはその引数が並列的に代入されたかのように機能するものだ:

> (let ((a 1))
(psetq a 2 b a)
(list a b))
(2 1)

そのため,ここではbaの古い値を得ている. マクロpsetqは,do等, 引数の幾つかを並列的に評価する必要のあるマクロの補助のために特に提供されたものだ. (setqを使っていたら,do*の定義になっていた.)

展開形を見れば,fooをループ用ラベルに使うのは本当はまずいことも明らかだ. foodoの実行本体のループ用ラベルに使われていたらどうするのか? 第9章で,この問題を詳細に取り扱う: 今の所は,マクロ展開ではfooではなく, 関数gensymの返す特別な無名シンボルを使わなければならない,と覚えておけばよい.

doを書くには,第\ref{fig:DesiredExpansionDo}図の上の式を 下の形に変形するには何が必要かを考えることになる. そのような変形を行うには, 逆クォート付きリストの正しい位置にマクロの仮引数を入れるだけでは済まない. 先頭のprogの次のリストはシンボルとその初期値から成るが, それらはdoの第2引数の中から抽出しなければならない. 第\ref{fig:ImplementDo}図のmake-initformsはそんなリストを返す. それからpsetqのために引数のリストを作らなければならないが, 更新すべきシンボルは全部ではないので,こちらの処理は複雑になる. 第\ref{fig:ImplementDo}図のmake-stepformsは,psetqに渡す引数を返す. これら2個の関数を使うと,残りの定義には込み入った技は要らない.

(defmacro our-do (bindforms (test &rest result) &body body)
  (let ((label (gensym)))
    `(prog ,(make-initforms bindforms)
           ,label
           (if ,test
               (return (progn ,@result)))
           ,@body
           (psetq ,@(make-stepforms bindforms))
           (go ,label))))

(defun make-initforms (bindforms)
  (mapcar #'(lambda (b)
              (if (consp b)
                  (list (car b) (cadr b))
                  (list b nil)))
          bindforms))

(defun make-stepforms (bindforms)
  (mapcan #'(lambda (b)
              (if (and (consp b) (third b))
                  (list (car b) (third b))
                  nil))
          bindforms))
\caption{doの実装例.} \label{fig:ImplementDo}

第\ref{fig:ImplementDo}図のコードは,実際の処理系でdoに使われている定義ではない. 展開中に行われる処理を強調するため, make-initformsmake-stepformsは独立した関数として分けておいた. いずれそんなコードも大抵はdefmacro内に残すようになるだろう.

このマクロの定義により,マクロに何ができるのかが見えてきた. マクロはLispの式を作る能力を最大限に生かしている. マクロの展開形を作るコードはそれ自身がプログラムであっても良いのだ.

マクロのスタイル

良いスタイルはマクロについては意味が違ってくる. スタイルが問題になるのはコードが人間に読まれるか,Lispに評価されるときだ. マクロの場合,その両方が普通とは少し違った状況で行われる.

マクロ定義に関わるコードには種類の異なる2個がある: 展開コード,つまりマクロが展開形を生成するために使うコードと, 被展開コード,つまり展開形そのものに現れるコードだ. スタイルの原則はそれぞれ違っている. 一般的に言って,プログラムの良いスタイルとは明確で効率的であることだ. これらの原則は2種類のマクロのコードにおいては別々の方向に向かって強調される: 展開コードは効率より明確さを優先し,被展開コードは明確さより効率を優先する.

効率性が一番問題になるのはコンパイル済みコードである訳だが, コンパイル済みコードの中ではマクロ呼び出しは既に展開済みだ. 展開コードが効率的ならコンパイルが幾分早く済むだろうが, プログラムの動作の効率には一切違いはない. マクロ呼び出しの展開はコンパイラの仕事のうち僅かの部分に過ぎないことが多いので, 効率的に展開されるマクロというのも普通はコンパイル速度にも大した影響を及ぼさない. だから大抵,展開コードはプログラムの初期ヴァージョンを手早く書く感じで書くことができる. 展開コードが不必要な処理をしたりコンシングを多く起こしたからといって,何だと言うのか? プログラマの時間はプログラムの別の場所を改良するのに費やした方が賢い. 展開コードにおいて明確さと効率のどちらかを選ぶことがあったら,明確さを優先すべきだ. マクロ定義は一般的に関数定義よりも読み下しづらいが, それは別々の二つの時点で評価される式が混じっているからだ. 展開コードの効率性の代わりにこの混乱が減らせるのなら,お得な取引と言える.

(defmacro our-and (&rest args)
  (case (length args)
    (0 t)
    (1 (car args))
    (t `(if ,(car args)
            (our-and ,@(cdr args))))))

(defmacro our-andb (&rest args)
  (if (null args)
      t
      (labels ((expander (rest)
                         (if (cdr rest)
                             `(if ,(car rest)
                                  ,(expander (cdr rest)))
                             (car rest))))
        (expander args))))
\caption{andと等価なマクロの例2個.} \label{fig:AndEquivs}

例えば一種のandをマクロとして定義したいとしよう. (and a b c)(if a (if b c))と等価だから, 第\ref{fig:AndEquivs}図の上の定義のように,andifを利用して書ける. 普通のコードを審査する基準からすれば,our-andの定義はまずい. 展開コードが再帰的で, 再帰のそれぞれの段階で同じリストの連続したcdr部の長さを求めている. このコードが実行時に評価されるなら, このマクロは同じ展開を一切無駄なく行うour-andbのように定義する方が良いだろう. しかしマクロ定義としてはour-andは(勝っているとは言えないが)同程度の出来だ. 再帰のそれぞれの段階でlengthを呼ぶのは非効率的だろうが, その構成は展開形が条件の数に依存する様子をはっきり表している.

何でもそうだが,例外もある. Lispではコンパイル時間と実行時間の区別は恣意的なもので, その区別に基づく法則はどれもやはり恣意的なものだ. プログラムによっては,コンパイル時間が実行時間なのだ. プログラムの主目的が何らかの形式の変換で,マクロをそのために利用しているなら, 話は全く違ってくる: 展開コードがプログラムの中心に,展開形がその出力になる, もちろんそんな状況では展開コードは効率性を念頭に置いて書かれるべきだ. しかし大抵の展開コードは (ア)コンパイル速度にのみ影響し,(イ)その影響も大したことはない ---つまり明確さがほぼ必ず一番重要だ.

被展開コードについては,全く逆になる. マクロの展開形は(特に他人には)読まれることはまず無いので,明確さは大した問題ではない. 禁断のgotoも展開形では必ずしも禁じられておらず, 評判の悪いsetqもそれ程嫌がられない.

構造化プログラミングの推進者達がgotoを憎むのは, それがソース・コードに及ぼした影響のせいだ. 彼らが有害だと見なすのはマシン語のジャンプ命令ではない ---ソース・コード内で抽象的構造に隠されている限りは問題にならないのだ. gotoがLispで非難の対象になっている理由は,楽に隠蔽できるからに他ならない: 代わりにdoを使えるし,doが備わっていなくとも自分で書くことができる. もちろん新しい抽象的構造をgotoの上に構築するなら, どこかにgotoが存在せざるを得ない. だから新しいマクロの定義にgoを使うのは, 既存のマクロが代わりに使えないときなら,必ずしも悪いことではない.

似たようにsetqも,変数がどこで値を得たのかが判り辛くなるので白い目で見られる. しかしマクロ展開は多くの人が読むものではないので, 普通はマクロ展開の中で創られた変数にsetqを使うことに害は少ない. 幾つかの組み込みマクロの展開形を見てみれば,setqが山程あるだろう.

状況によっては展開形コードでの明確さが一層大事になることがある. 複雑なマクロを書くときは(少なくともデバッグ中に)結局展開形を読む羽目になる. また単純なマクロでも,展開コードと被展開コードの区別は単なる逆クォートなので, そんなマクロが格好悪い展開形を生成するのなら, ソース・コード内でも格好悪さがはっきり見えてしまうだろう. しかし被展開コードの明確さが問題になるときでも,効率性が依然として優先するべきだ. プログラム実行時に評価される大部分のコードの中では,効率性が重要なのだ. マクロ展開では二つの理由からそれが際立つ: マクロがどこででも使われるせいと,マクロが不可視なせいだ. マクロはしばしば一般用途のユーティリティを実装するのに使われるが, それらはプログラムのあらゆる所で呼び出されるものだ. それ程頻繁に使われるものは非効率的なままで放って置く訳にはいかない. 無害で小さなマクロに見えるものが,全て展開された後には, プログラム内で無視できない分量を占める. そんなマクロには,長さから判断して必要と思えるよりも多くの注意が必要だ. 特にコンシングを避けること. 不必要なコンシングを起こすユーティリティは, それさえなければ効率的なプログラムの動作を鈍化させることにもなりかねない.

展開形コードの効率性に目を向ける理由のもう一つは,それが非常に見え辛い点だ. 関数の実装方法が下手でも,その関数がプログラマの目に入る度にその事実は意識される. しかしマクロはそうではない.マクロ定義からは展開形の効率の悪さは明らかでないこともある. 展開形コードの非効率性は明らかにならないかも知れないので,一層注意して見るべきなのだ.

マクロへの依存

関数定義を書き直すと,それを呼び出す関数は自動的に更新後の関数を使うようになる \footnote{インラインでコンパイルされた関数は別で, 再定義に関してマクロと同様の制限を負っている.}. ただしマクロについては必ずしも同じようには行かない. 関数定義内にあるマクロ呼び出しは関数がコンパイルされるときに展開形に置換される. マクロを呼び出す関数がコンパイルされた後にそのマクロを再定義したらどうなるだろうか? 元々のマクロ呼び出しの形跡は残っていないので,関数内の展開形は更新されない. 関数の動作は古いマクロ定義を反映し続けることになる:

> (defmacro mac (x) `(1+ ,x))
MAC
> (setq fn (compile nil `(lambda (y) (mac y))))
#<Compiled-Function BF7E7E>
> (defmacro mac (x) `(+ ,x 100))
MAC
> (funcall fn 1)
2

マクロを呼ぶコードがマクロそのものの定義前にコンパイルされると似た問題が起こる. CLtL2には 「マクロ定義は,マクロの最初の使用よりも先にコンパイラに読まれていなければならない」 とある. 幸運なことにどちらの問題も避けるのは容易だ. 以下の二つの原則を忘れなければ, 古かったり存在しないマクロ定義について思い悩むことはない:

  1. マクロはそれを呼び出す関数(またはマクロ)の前で定義する.
  2. マクロが再定義されたときは, それを(直接または間接的に)呼び出している関数とマクロを全て再コンパイルする.

マクロ定義が確かに最初にコンパイルされるようにするため, 一つのプログラム内で使われるマクロを全て独立したファイルに分ける方法が これまで提案されてきた. しかしそれはやり過ぎだ. whileのような汎用マクロを独立したファイルに分けるのは理に適っているが, 汎用ユーティリティは関数であれマクロであれ, プログラムの他の部分とは分けておく方がよい.

マクロにはプログラムのある特定の部分のためだけに書かれたものもあるので, それらは使う側のコードと一緒に定義されるべきだ. マクロ定義が呼び出しのどれよりも前に現れる限りコンパイルは上手く行く. マクロは関数と違うからというだけの理由で全て一緒にしておいても, コードが読み辛くなるだけのことだ.

関数からマクロへ

この章では関数をマクロに翻訳する方法を扱う. 最初の1歩は,本当にその必要があるかどうか自問することだ. 関数をインライン宣言するだけで済むのでないだろうか(p. 26)?

しかし関数をマクロに翻訳する方法を考えることにはしっかりした理由がある. マクロを書きはじめると,関数を書いているような気がしてくることがある. 普通このアプローチでは完璧なマクロは書けないが,少なくともこの感じは手がかりになる. マクロと関数の関係を理解しておきたい理由には,他にも,それらの違いを理解したいというのもある. 最後に,Lispプログラマにはとにかく関数をマクロに書き換えたいことが時々あるものなのだ.

関数をマクロに翻訳することの難しさは,その関数の持つ性質の数による. 最も翻訳が簡単な部類の関数は

  1. 本体が単一の式から成っていて
  2. 仮引数リストには仮引数の名前のみが含まれ
  3. (仮引数以外に)新しい変数を創らず
  4. 再帰的でなく(また相互再帰的な関数のグループにも含まれず)
  5. 本体内で複数回使われる仮引数を持たず
  6. 仮引数リスト内で先に現れる仮引数の値より, 後に現れる仮引数の値の方が本体内で先に使われることがなく
  7. フリー変数を含まない

ものだ. これらの原則を満たす関数の例はCommon Lispの組み込み関数secondで, これはリストの第2要素を返す. その関数はこう定義すればよい:

(defun second (x) (cadr x))

この場合関数定義は上の条件を全て満たしているので,等価なマクロ定義に翻訳するのは簡単だ. 逆クォートを関数本体の前に付け, 仮引数リストに含まれていたシンボルが出て来る度にコンマを付けるだけでよい:

(defmacro second (x) `(cadr ,x))

もちろんこのマクロは関数とまったく同じ条件で呼び出すことはできない. applyfuncallの第1引数としては渡せないし, 呼出側の関数が新たなローカルな束縛を生むような環境では使うべきでない. それでも普通のインライン呼び出しでは, マクロsecondは関数secondと同じ動作をするはずだ.

マクロは単一の式に展開されなければならないので, 本体に複数の式が含まれるときには少し違う方法を使う. 条件1が満たされないときにはprognを追加すればよい. つまり関数noisy-secondは:

(defun noisy-second (x)
  (princ "Someone is taking a cadr!")
  (cadr x))

次のマクロ定義として翻訳できる:

(defmacro noisy-second (x)
  `(progn
     (princ "Someone is taking a cadr!")
     (cadr ,x)))

関数が&restまたは&body仮引数を使っていて条件2が満たされないときは, 仮引数に関してだけ方法を変える. ただコンマを前に付けるのではなく, 仮引数の中身がlistの呼び出しの中に切り貼りされなければならない. よって

(defun sum (&rest args)
  (apply #'+ args))

(defmacro sum (&rest args)
  `(apply #'+ (list ,@args)))

となるが,この場合は次のようにした方がよい:

(defmacro sum (&rest args)
  `(+ ,@args))

条件3が満たされないとき ---関数本体内で新しい変数が創られるとき--- は, コンマを付ける方法を修正しなければならない. 仮引数リストに含まれるシンボル全ての前にコンマを付けるのではなく, 仮引数を参照しているものにのみコンマを付ける. 例えば次の例では

(defun foo (x y z)
  (list x (let ((x y))
            (list x z))))

xの内後ろの二つは仮引数のxを参照していない. 2番目のxは一切評価の対象にならず, 3番目はletの創り出した新しい変数を参照している. だから1番目にだけコンマを付ける:

(defmacro foo (x y z)
  `(list ,x (let ((x ,y))
              (list x ,z))))

条件4〜6が満たされなくとも関数をマクロに翻訳できることもあるが, これらの内容は後で独立して扱う. マクロにおける再帰については第10.4章で, 評価の重複と評価順の混乱の危険性についてはそれぞれ第10.1章と10.2章で扱う.

条件7については,xienページで触れたエラーに似た技を使ってクロージャをマクロで真似ることができる. しかしこれは程度の低い技であり,この本の基調に馴染まないので, 詳しく扱うことはしない.

シンボル・マクロ

CLtL2でCommon Lispに新種のマクロが導入された ---シンボル・マクロだ. 普通のマクロ呼び出しの見掛けが関数呼び出しに似ているのに対して, シンボル・マクロの「呼び出し」の見掛けはシンボルに似ている.

シンボル・マクロはローカルにのみ定義できる. スペシャル式symbol-macroletを使うと, その本体内では唯のシンボルが式のように振る舞うようになる:

> (symbol-macrolet ((hi (progn (print "Howdy")
                               1)))
                   (+ hi 2))
"Howdy"
3

symbol-macroletの本体は引数の場所にあるhiが 全て(progn (print "Howdy") 1)で置き換えられたかのように評価される.

概念的には,シンボル・マクロは引数をとらないマクロに似ている. 引数がなければ,マクロは見掛け上の省略形に過ぎない. しかしシンボル・マクロが無益だと言いたいのではない. それは第15章と第18章で使われているが,後者では必要不可欠な役目を持っている.


←: 表現としての関数     ↑: On Lisp     →: いつマクロを使うべきか

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