コード内に生じたパターンは,しばしば新しい抽象化が必要であることを知らせてくれる. このルールはマクロ自身のコードについても全く同じく当てはまる. 幾つかのマクロが似た形で定義されているときは, マクロを定義するマクロを書いて,それらを生成させることができるかも知れない. 省略名を定義するマクロ,アクセス用マクロを定義するマクロ, そして第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))
dbind
とmvbind
の定義は,どれ程似通っているか注意して欲しい.
実際,任意の関数
\footnote{省略形はapply
やfuncall
には渡せないが.},
マクロや特殊式の省略形を定義するには,
この形の&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)))
最後に,内側の逆クォート内の',long
をname
に置き換えて式を簡略化する.
`(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
を使うには余計な括弧を挿入する必要はない.
abbrevs
はgroup
(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))
前節の省略形と同様に,マクロcolor
とweight
の定義はほぼ同一だ.
ここで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節では幾つかのアナフォリックマクロの定義を示した.
aif
やaand
のようなマクロを使うときは,
何らかの引数の評価中にはシンボル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個のアナフォリックマクロを定義した.
aif
,awhen
,awhile
,acond
,alambda
,
ablock
そして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式を生成することだ.
個々のlet
はit
を異なる引数に束縛したままにするが,
個別の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
ではaif
やawhile
は定義できないが,
どのような関数のアナフォリック版でも定義できる.
a+
がa+expand
を呼んで展開形を生成していたのと同様,
defanaph
はanaphex
を呼んで展開形を生成する.
汎用の展開関数anaphex
がa+expand
と違っているのは,
最終的に展開形内に現れる関数名を引数に取る点だけだ.
実際,a+
は次のように定義できる.
(defmacro a+ (&rest args) (anaphex args '(+)))
anaphex
とa+expand
のいずれも独立した関数として定義する必要はない.
anaphex
はlabels
やalambda
によってdefanaph
内部で定義できる.
展開形を生成する関数が上で分離されていたのは,コードを明確にするためだけだ.
普通,defanaph
は引数の先頭1文字(おそらくa
)を取り除いたものが
展開形内で呼び出されるようにする.
(これはpop-symbol
によって行われる.)
ユーザが代わりを指定したいときは,オプショナル引数で指定できる.
defanaph
は全ての関数と幾つかのマクロのアナフォリック版を定義できるが,面倒な制限が伴う.
it
は常に1個前の引数に束縛される.
しかし場合によっては ---例えばawhen
---
it
は第1引数の値に束縛されたままであって欲しい.
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
で指定する.
すなわち,それによってマクロ呼び出しの引数を評価する際の規則を指定する.
それぞれの方式は次の通り.
alist
用の方式を取る.
マクロ呼び出しの引数は全て評価され,it
は常に直前の引数の値に束縛される.
aif
用の方式を取る.
必ず評価されるのは第1引数のみで,it
はその値に束縛される.
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)))