マクロを定義するマクロ

コード内に生じたパターンは,しばしば新しい抽象化が必要であることを知らせてくれる. このルールはマクロ自身のコードについても全く同じく当てはまる. 幾つかのマクロが似た形で定義されているときは, マクロを定義するマクロを書いて,それらを生成させることができるかも知れない. 省略名を定義するマクロ,アクセス用マクロを定義するマクロ, そして第14.1節で説明した類のアナフォリックマクロを定義するマクロだ.

省略

マクロの用途の一番単純なものは,省略形として使うものだ. Common Lispのオペレータにはかなり長い名前のものがある. その中で高い順位を誇るものが(最長とはとても言えないが) destructuring-bindで,18文字になる. Steeleの原則(sjlページ)から導かれる「系」は, よく使われるオペレータの名前は短くあるべきということだ. (「加算操作をコストの低いものと思う理由の一つは, それを1文字 "+" で表記できることだ.」) 組み込みマクロdestructuring-bindは新たな抽象化の層をもたらすが, 簡潔さの面では実際の利益はその長い名前に覆い隠されてしまう.

(let ((a (car x)) (b (cdr x))) ...)

(destructuring-bind (a . b) x ...)

プログラムは,印刷された文章と同様,1行がせいぜい70文字のときが一番読み易い. 1個の識別子がその1/4を占めるようなときには,始めから悪条件の下に置かれたことになる.

幸運なことに,Lispのようなプログラミング言語では,言語設計者の決定に全てを任せて生きる必要はない. 次のように定義しておけば,

(defmacro dbind (&rest args)
  `(destructuring-bind ,@args))

もう2度と長い名前を使う必要はない. 更に長く,しかもよく使われるmultiple-value-bindについても同様だ.

(defmacro mvbind (&rest args)
  `(multiple-value-bind ,@args))

dbindmvbindの定義は,どれ程似通っているか注意して欲しい. 実際,任意の関数 \footnote{省略形はapplyfuncallには渡せないが.}, マクロや特殊式の省略形を定義するには, この形の&rest引数とコンマ・アットで十分なのだ. 作業を代わりに行ってくれるマクロが手に入るというのに, どうして更にmvbindの方式で定義を作り出さなければならないことがあろうか?

マクロを定義するマクロを定義するには,しばしば入れ子になった逆クォートが必要になる. 逆クォートの入れ子は理解し辛いことで悪評が高い. よく使われる形にはいつか慣れるだろうが, 逆クォートの付いた任意の式を見て,どのように展開されるかを言えるようになるとは思うべきではない. そうなるのはLispの欠陥ではなく,ましてや表記の欠陥でもない. 込み入った積分の数式を見て値が何か知ることができないのと同じことだ. 困難は問題の中にあり,表記の中にあるのではない.

(defmacro abbrev (short long)
  `(defmacro ,short (&rest args)
     `(,',long ,@args)))

(defmacro abbrevs (&rest names)
  `(progn
     ,@(mapcar #'(lambda (pair)
                    `(abbrev ,@pair))
                (group names 2))))
\caption{省略形の自動定義.} \label{fig:AutomaticDefOfAbbrev}

しかし積分に取り組むときと同様,逆クォートの分析を小さな段階に分け, それぞれは容易に構成を追えるようにすることはできる. ここでマクロabbrevを書きたいとしよう. これはmvbindをただ次のようにするだけで定義できるようにしてくれるものだ.

(abbrev mvbind multiple-value-bind)

第\ref{fig:AutomaticDefOfAbbrev}図には,このマクロの定義を示した. この定義は何から出てきたのだろうか? このようなマクロの定義は,展開例から導ける. 展開例は次のようになる.

(defmacro mvbind (&rest args)
  `(multiple-value-bind ,@args))

逆クォートの中からmultiple-value-bindを取り出すと,導出は簡単になる. 最終的に作られるマクロではそれが引数になることが分かっているからだ. こうして,次の等価な定義が得られる.

(defmacro mvbind (&rest args)
  (let ((name 'multiple-value-bind))
    `(,name ,@args)))

これでこの式をテンプレートにすることができる. 先頭に逆クォートを付け,変化し得る式を変数に置き換える.

`(defmacro ,short (&rest args)
   (let ((name ',long))
     `(,name ,@args)))

最後に,内側の逆クォート内の',longnameに置き換えて式を簡略化する.

`(defmacro ,short (&rest args)
   `(,',long ,@args))

これで第\ref{fig:AutomaticDefOfAbbrev}図に示したマクロの本体が得られる.

第\ref{fig:AutomaticDefOfAbbrev}図にはabbrevsも示した. これは複数の省略形を同時に定義したいときに使う.

(abbrevs dbind  destructuring-bind
         mvbind multiple-value-bind
         mvsetq multiple-value-setq)

abbrevsを使うには余計な括弧を挿入する必要はない. abbrevsgroup(aqhページ)を呼び,引数を2個ごとにまとめるからだ. 一般に,論理的には不要な括弧を打ち込むのをマクロが省いてくれるのはよいことだ. groupはそういったマクロの大部分に役立つ.

属性

Lispは,何かの属性をオブジェクトに関連づける方法を数多く提供する. 問題とされるオブジェクトがシンボルとして表現されるときは, 一番便利な方法は(一番効率が悪いが)シンボルの属性リストを使うものだ. オブジェクトoが属性pを持ち,その値がvであるという事実を表現するには, oの属性リストを設定する.

(setf (get op) v)

だからball1が色redを持つことを表現するには,こうすればよい.

(setf (get 'ball1 'color) 'red)

オブジェクトの属性のうち頻繁に参照するものがあるときは,それを参照するマクロを定義し,

(defmacro color (obj)
   `(get ,obj 'color))

そしてgetの場所にcolorを使う.

> (color 'ball1)
RED

マクロ呼び出しはsetfに対して透明なので(第12章を参照)こうも書ける.

> (setf (color 'ball1) 'green)
GREEN

そのようなマクロには, プログラムがオブジェクトの色を表現するのに使う特定の方法を隠蔽できるという利点がある. 属性リストは遅い. プログラムの今後のヴァージョンでは,速度を考慮し, 色を構造体のフィールドまたはハッシュ表の項目として表現することになるかも知れない. colorのように,データをそれに被せたマクロ経由で扱うと, かなり構築の進んだプログラムにおいてすら, 最下層のコードに大きな変更を加えることが容易になる. プログラムで使うものが属性リストから構造体に移行しても, そこに被せたアクセス用マクロより上の段階に変更を加える必要はない. 被せたマクロを利用するコードでは,内部で起こった再構築を意識する必要すらない.

(defmacro propmacro (propname)
  `(defmacro ,propname (obj)
     `(get ,obj ',',propname)))

(defmacro propmacros (&rest props)
  `(progn
     ,@(mapcar #'(lambda (p) `(propmacro ,p))
                props)))
\caption{アクセス用マクロの自動定義.} \label{fig:AutoDefAccessMacro}

重さという属性に対しては,色に対して書いたものと似たマクロを定義すればよい.

(defmacro weight (obj)
  `(get ,obj 'weight))

前節の省略形と同様に,マクロcolorweightの定義はほぼ同一だ. ここでpropmacro(第\ref{fig:AutoDefAccessMacro}図)はabbrevと同じ役割を担う.

マクロを定義するマクロを構成する過程には他のどのマクロとも違いはない. マクロ呼び出しを,次に展開されて欲しい式を見て, そして前者を後者に変形する方法を理解することだ. 次のマクロは,

(propmacro color)

次のように展開されて欲しい.

(defmacro color (obj)
  `(get ,obj 'color))

この展開形そのものにもdefmacroが使われているが, テンプレートの作り方はやはり次の通りだ. 展開形に逆クォートを付け,colorが使われている所にコンマを付けた仮引数名を置く. 前節と同様,既存の逆クォートの中にcolorが現れないように展開形を変形することから始める.

(defmacro color (obj)
  (let ((p 'color))
    `(get ,obj ',p)))

そうしたら更に手順を進めてテンプレートを作り,

`(defmacro ,propname (obj)
   (let ((p ',propname))
     `(get ,obj ',p)))

これを簡略化して次を得る.

`(defmacro ,propname (obj)
   `(get ,obj ',',propname))

一群の属性名が全てマクロとして定義されなければならない場合のために, propmacros (\ref{fig:AutoDefAccessMacro}) がある. これは複数個のpropmacroを個別に呼び出すコードに展開される. abbrevsと同様,実際にコードの大部分はマクロを定義するマクロを定義するマクロだ.

この節では属性リストを扱ったが,ここで扱った技法は一般的なものだ. これを使って,どのような形で保持されているデータに対してもアクセス用マクロを定義できる.

アナフォリックマクロ

第14.1節では幾つかのアナフォリックマクロの定義を示した. aifaandのようなマクロを使うときは, 何らかの引数の評価中にはシンボルitが他のシンボルの返した値に束縛される. よってこうする代わりに,

(let ((res (complicated-query)))
  (if res
      (foo res)))

ただこうすればよく,

(aif (complicated-query)
         (foo it))

またこうする代わりに,

(let ((o (owner x)))
  (and o (let ((a (address o)))
           (and a (city a)))))

ただこうすればよい.

(aand (owner x) (address it) (city it))

第14.1節では7個のアナフォリックマクロを定義した. aifawhenawhileacondalambdaablockそしてaandだ. この種のアナフォリックマクロのうち,便利なものはこれら7個だけなどということはない. 実際,Common Lispのあらゆる関数やマクロについてアナフォリック版を定義できる. これらのマクロの多くはmapconのようになるだろう. つまり滅多に使われないが,必要なときには欠かせないようなものだ.

例えばaandと同様に, itが常に前の引数の返した値に束縛されるようなa+というものを定義できる. 次の関数はマサチューセッツで食事を取るときの費用を計算する.

(defun mass-cost (menu-price)
  (a+ menu-price (* it .05) (* it 3)))

マサチューセッツ食料税が5%,また住民はしばしば税の3倍をチップとして払う. この法則に従うと,Dolphin Seafoodでbroiled scrodを食べるときの費用はこうなる.

> (mass-cost 7-95)
9-54

しかしこれはsaladとbaked potatoも含んでいる.

(defmacro a+ (&rest args)
  (a+expand args nil))

(defun a+expand (args syms)
  (if args
      (let ((sym (gensym)))
        `(let* ((,sym ,(car args))
                (it ,sym))
           ,(a+expand (cdr args)
                      (append syms (list sym)))))
      `(+ ,@syms)))

(defmacro alist (&rest args)
  (alist-expand args nil))

(defun alist-expand (args syms)
  (if args
      (let ((sym (gensym)))
        `(let* ((,sym ,(car args))
                (it ,sym))
           ,(alist-expand (cdr args)
                          (append syms (list sym)))))
      `(list ,@syms)))
\caption{a+alistの定義.} \label{fig:DefAPlusAlist}

第\ref{fig:DefAPlusAlist}に示したマクロa+は, 展開形の生成を再帰関数a+expandに依存している. a+expandの戦略を大雑把に言うと, マクロ呼び出しの引数から成るリストのcdr部に対して再帰的に働き, 一連の入れ子になったlet式を生成することだ. 個々のletitを異なる引数に束縛したままにするが, 個別のgensymをそれぞれの引数に束縛する. 展開関数はそれらのgensymをリストにまとめ, 引数のリストの終端に達すると,gensymを引数にした+式を返す. よって次の式は,

(a+ menu-price (* it .05) (* it 3))

次の展開形を生成する.

(let* ((#:g2 menu-price) (it #:g2))
  (let* ((#:g3 (* it 0-05)) (it #:g3))
        (let* ((#:g4 (* it 3)) (it #:g4))
           (+ #:g2 #:g3 #:g4))))

第\ref{fig:DefAPlusAlist}図には,これと似たalistも示した.

> (alist 1 (+ 2 it) (+ 2 it))
(1 3 5)
(defmacro defanaph (name &optional calls)
  (let ((calls (or calls (pop-symbol name))))
    `(defmacro ,name (&rest args)
       (anaphex args (list ',calls)))))

(defun anaphex (args expr)
  (if args
      (let ((sym (gensym)))
        `(let* ((,sym ,(car args))
                (it ,sym))
           ,(anaphex (cdr args)
                     (append expr (list sym)))))
      expr))

(defun pop-symbol (sym)
  (intern (subseq (symbol-name sym) 1)))
\caption{アナフォリックマクロの自動定義.} \label{fig:AutoDefOfAnaphMac}

繰り返すが,a+alistの定義はほぼ同一だ. そのようなマクロをもっと定義したいようなときは,それらのほとんどがコードの重複になるだろう. プログラムにそれらを生成させればよいではないか? 第\ref{fig:AutoDefOfAnaphMac}図のマクロdefanaphがそれを行ってくれる. defanaphを使うと,a+alistの定義は次のように簡潔になる.

(defanaph a+)
(defanaph alist)

こうして定義されたa+alistの展開形は, 第\ref{fig:DefAPlusAlist}図のコードによる定義と同一だ. マクロを定義するマクロdefanaphは, 引数が関数と同じ通常の評価法則にしたがって評価されるものならば何であってもアナフォリック版を作れる. すなわち,defanaphの対象は,引数が全て評価され,かつ左から右に評価されるものならば何でもよい. だから上に示したdefanaphではaifawhileは定義できないが, どのような関数のアナフォリック版でも定義できる.

a+a+expandを呼んで展開形を生成していたのと同様, defanaphanaphexを呼んで展開形を生成する. 汎用の展開関数anaphexa+expandと違っているのは, 最終的に展開形内に現れる関数名を引数に取る点だけだ. 実際,a+は次のように定義できる.

(defmacro a+ (&rest args)
  (anaphex args '(+)))

anaphexa+expandのいずれも独立した関数として定義する必要はない. anaphexlabelsalambdaによってdefanaph内部で定義できる. 展開形を生成する関数が上で分離されていたのは,コードを明確にするためだけだ.

普通,defanaphは引数の先頭1文字(おそらくa)を取り除いたものが 展開形内で呼び出されるようにする. (これはpop-symbolによって行われる.) ユーザが代わりを指定したいときは,オプショナル引数で指定できる. defanaphは全ての関数と幾つかのマクロのアナフォリック版を定義できるが,面倒な制限が伴う.

  1. 引数が全て評価されるオペレータにしか適用できない.
  2. 展開形内では,itは常に1個前の引数に束縛される. しかし場合によっては ---例えばawhen--- itは第1引数の値に束縛されたままであって欲しい.
  3. setfのように,第1引数に汎変数を取るマクロには適用できない.

これらの制限を幾つか取り除く方法を考えよう. 第1の問題の一部は,第2の問題に帰着される. aifのようなマクロを定義する展開形を生成するには, anaphexがマクロ呼び出しの第1引数のみを置換するように修正する必要がある.

(defun anaphex2 (op args)
  `(let ((it ,(car args)))
     (,op it ,@(cdr args))))

上の再帰的でないanaphexは, itがマクロ展開によって一連の引数に必ず束縛されるようにする必要がないので, マクロ呼び出しの引数を必ずしも全て評価しないような展開形を生成できる. 評価する必要があるのは第1引数だけだ (これはitをその値に束縛するため). よってaifは次のように定義できる.

(defmacro aif (&rest args)
  (anaphex2 'if args))

xqgページの元の定義との違いは,こちらではaifに誤った数の引数を与えるとエラーになる点だけだ. 正しいマクロ呼び出しでは,どちらも同一の展開形を生成する.

defanaphが汎変数に対して動作しないという3番目の問題は, 展開形内でf(gvxページ)を使うと解決できる. setfなどのオペレータは,anaphex2を次のように定義し直したものなら扱える.

(defun anaphex3 (op args)
  `(_f (lambda (it) (,op it ,@(cdr args))) ,(car args)))

この展開関数はマクロ呼び出しが1個以上の引数を持つものと仮定している (第1引数を汎変数にする). これを使うと,asetfは次のように定義できる.

(defmacro asetf (&rest args)
       (anaphex3 'setf args))
(defmacro defanaph (name &optional &key calls (rule :all))
  (let* ((opname (or calls (pop-symbol name)))
         (body (case rule
                 (:all   `(anaphex1 args '(,opname)))
                 (:first `(anaphex2 ',opname args))
                 (:place `(anaphex3 ',opname args)))))
    `(defmacro ,name (&rest args)
       ,body)))

(defun anaphex1 (args call)
  (if args
      (let ((sym (gensym)))
        `(let* ((,sym ,(car args))
                (it ,sym))
           ,(anaphex1 (cdr args)
                      (append call (list sym)))))
      call))

(defun anaphex2 (op args)
  `(let ((it ,(car args))) (,op it ,@(cdr args))))

(defun anaphex3 (op args)
  `(_f (lambda (it) (,op it ,@(cdr args))) ,(car args)))
\caption{更に一般的なdefanaph.} \label{fig:MoreGeneralDefanaph}

第\ref{fig:MoreGeneralDefanaph}図には,3種類の展開関数を示したが, それらは単一のマクロである新版defanaphによって統合されている. プログラマはマクロ展開の望みの方式を,オプショナルなキーワード引数ruleで指定する. すなわち,それによってマクロ呼び出しの引数を評価する際の規則を指定する. それぞれの方式は次の通り.

:all
(デフォルト)マクロ展開はalist用の方式を取る. マクロ呼び出しの引数は全て評価され,itは常に直前の引数の値に束縛される.
:first
マクロ展開はaif用の方式を取る. 必ず評価されるのは第1引数のみで,itはその値に束縛される.
:place
マクロ展開はasetf用の方式を取る. 第1引数は汎変数として扱われ,itはその初期値に束縛される.

新しいdefanaphを使うと,ここまでに示した例の幾つかは次のように定義できる.

(defanaph alist)
(defanaph aif :rule :first)
(defanaph asetf :rule :place)

asetfには,汎変数を使う多種多様なマクロを複数回の評価について心配せずに定義できる長所がある. 例えば,incfは次のように定義できる.

(defmacro incf (place &optional (val 1))
  `(asetf ,place (+ it ,val)))

また,例えばpull(sqkページ)は次のように定義できる.

(defmacro pull (obj place &rest args)
  `(asetf ,place (delete ,obj it ,@args)))

←: 関数を返すマクロ     ↑: On Lisp     →: リードマクロ

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