第8章では,マクロの利点の1つは引数を変形できる能力だということに触れた.
その種のマクロの1つがsetf
だ.
この章では,setf
の持つ隠れた意味に視点を向け,その上に構築できるマクロの例を幾つか示す.
setf
の上に適切なマクロを構築することは,驚く程難しい.
この話題への導入として,最初の節では少々まずい所のある単純な例を示す.
次の節ではそれの何がまずいのかを説明し,改善方法を示す.
その後の2節ではsetf
の上に構築されたユーティリティの例を提示し,
最後の節でsetf
の逆に当たる独自のオペレータの定義方法を説明する.
組込みマクロsetf
はsetq
の一般化に当たる.
setf
の第1引数はただの変数でなく関数やマクロの呼び出しであってもよい.
> (setq lst '(a b c)) (ABC) > (setf (car lst) 480) 480 > lst (480 B C)
一般的には(setf x y)
は
「x
がy
に評価されるようにしておいてくれ」という指示として理解できる.
setf
はマクロなので,引数の内部を見てそのような指示を実現するには何をすればよいかを判断できる.
(展開後の)1引数がシンボルだったら,setf
は単にsetq
に展開される.
しかし第1引数が別の変数を求めるための式だったときは,
setf
は対応する確定した命令(assertion)に展開される.
第2引数は定数なので,前の例は次のように展開されるかもしれない.
(progn (rplaca lst 480) 480)
このように変数を求める式から決定的な命令へ変換することはインヴァージョン(inversion)と呼ばれる.
car
,cdr
,nth
,aref
,get
,gethash
及び
defstruct
で定義されるアクセス関数等,Common Lispで頻繁に使われる,
オブジェクトにアクセスするための関数にはインヴァージョンに当たるものが予め定義されている.
(完全な一覧はCLtL2の125ページにある)
setf
の第1引数として機能する式は汎変数(generalized variable)と呼ばれる.
汎変数は強力な抽象化手段であることが分かっている.
インヴァージョン可能な参照に展開されるマクロ呼び出しは全てそれ自体がインヴァージョン可能という点で,
マクロ呼び出しは汎変数と似ている.
またsetf
の上にマクロも構築すれば,相乗効果によってかなり明確なプログラムができる.
setf
の上に構築できるマクロの1つはtoggle
だ
\footnote{以下の節で見るように,この定義は正しくない.}.
(defmacro toggle (obj) ; 間違い `(setf ,obj (not ,obj)))
これは汎変数の値を切替える:
> (let ((lst '(a b c))) (toggle (car lst)) lst) (NIL B C)
さて次のアプリケーションを考えよう. 誰かが ---昼のドラマ劇場の脚本家,精力に溢れた社交界の人間,はたまた政党の事務員か--- ---小さな町の住人の間の人間関係を全て記録したデータベースを運営したいとしよう. テーブルには住人の友人を記録する欄が必要だ.
(defvar *friends* (make-hash-table))
このハッシュ表のエントリはそれ自身がハッシュ表で,
ここには友人の候補の名前がt
またはnil
に結合されている.
(setf (gethash 'mary *friends*) (make-hash-table))
JohnをMaryの友人として登録するには,次のようにすることになる.
(setf (gethash 'john (gethash 'mary *friends*)) t)
町は2つのグループに分けられる. グループというものはいつでも互いに「仲間じゃないやつは敵だ」と言いたがるので, 町の人間はどちらかのグループに属することを余儀なくされる. だから誰かがグループを移ると,元の友人は敵に,敵は友人に変わる.
組込みオペレータだけによってx
がy
の友人かどうかを切替えるには,
次のようにすることになる.
(setf (gethash x (gethash y *friends*)) (not (gethash x (gethash y *friends*))))
これはかなり複雑な式だ
(もちろんsetf
なしではこの程度では済まなかっただろうが).
データベースに次のようなアクセス用マクロを定義していれば,
(defmacro friend-of (p q) `(gethash ,p (gethash ,q *friends*)))
このマクロとtoggle
とで,データベースの変更を扱うためにはかなり準備が整ったことになる.
上の更新操作は簡潔に表現できるだろう.
(toggle (friend-of x y))
汎変数は美味しい健康食品のようなものだ.
これによってできるプログラムは見事にモジュール化され,それでいて美しくエレガントだ.
データ構造にマクロかインヴァージョン可能な関数を通じてアクセスを提供すれば,
他のモジュールはデータ表現の詳細を知る必要のないままsetf
でデータ構造を操作できる.
前の節ではtoggle
の最初の定義はまずい所があると警告した.
(defmacro toggle (obj) ; 間違い `(setf ,obj (not ,obj)))
これは第10.1節で説明した問題,つまり複数回の評価の元になる.
問題は引数に副作用があるときに起きる.
例えばlst
がオブジェクトから成るリストで,それを使ってこう書いたとき,
(toggle (nth (incf i) lst))
$(i+1)$番目の要素の値を切替えているものと思うだろう.
しかしtoggle
の現在の定義ではこのマクロ呼び出しは次のように展開される.
(setf (nth (incf i) lst) (not (nth (incf i) lst)))
これはi
を2回増加させ,$(i+1)$番目の要素を$(i+2)$番目の要素の逆に設定する.
よって次の例では,
> (let ((lst '(t nil t)) (i -1)) (toggle (nth (incf i) lst)) lst) (T NIL T)
toggle
を呼んでも効果はないようだ.
toggle
の引数として与えられた式を取り出し,
setf
の第1引数の場所に挿入するだけでは十分ではない.
式の内部を見て,その動作を知らなければならないのだ.
それが部分式を含むなら,それらが副作用を持つことを考慮し,分割して別個に評価しなければならない.
一般的に言えば,これは複雑な仕事だ.
余計な手間を省くため,
Common Lispはsetf
の上に,ある限られた範囲のマクロを自動的に定義するマクロを提供している.
このマクロはdefine-modify-macro
という名前で,3個の引数を取る.
定義したいマクロの名前,(汎変数の後に)取るかもしれない付加的な引数,
そして汎変数の新しい値を返す関数の名前だ
\footnote{これは一般的な意味での関数名だ:
1+
または(lambda (x) (+ x 1))
のどちらでもよい.}.
define-modify-macro
を使うと,define
はこうして定義できる.
(define-modify-macro toggle () not)
換言すれば,これは「(toggle place)
の形の式を評価するには,
place
の指定する位置を見つけ,そしてそこに蓄えられた値がval
ならば,
それを(not val)
の値で置き換えること」と指示していることになる.
次は新しいマクロを同じ例に使ったところだ.
> (let ((lst '(t nil t)) (i -1)) (toggle (nth (incf i) lst)) lst) (NIL NIL T)
この定義は正しい結果をもたらすが,更に一般化することもできる.
setf
とsetq
は任意の数の引数を取れるのだから,toggle
もそうあるべきだ.
第\ref{fig:MacrosOnGnrlVars}に示したように,
この機能はdefine-modify-macro
の上に別のマクロを定義することで追加できる.
この節では汎変数に対して機能する新しいユーティリティの例を幾つか示す.
引数をsetf
にそのままの形で渡すためには,それらはマクロでなければならない.
(defmacro allf (val &rest args) (with-gensyms (gval) `(let ((,gval ,val)) (setf ,@(mapcan #'(lambda (a) (list a gval)) args))))) (defmacro nilf (&rest args) `(allf nil ,@args)) (defmacro tf (&rest args) `(allf t ,@args)) (defmacro toggle (&rest args) `(progn ,@(mapcar #'(lambda (a) `(toggle2 ,a)) args))) (define-modify-macro toggle2 () not)\caption{汎変数に対して機能するマクロ.} \label{fig:MacrosOnGnrlVars}
第\ref{fig:MacrosOnGnrlVars}には,setf
の上に定義した新しいマクロを4個示した.
最初のallf
は,複数の汎変数を同じ値に設定するためのものだ.
さらにその上にnilf
とtf
が作られた.
これらは引数をそれぞれnil
とt
に設定する.
これらは単純なマクロだが,プログラムに大きな違いを生む.
setq
と同様,setf
も複数の引数を取ることができる.
すなわち他に設定したい変数と値の組だ.
(setf x 1 y 2)
これらのユーティリティも同様だが,半分の引数を省略することもできる.
複数の変数をnil
に設定したいときは,
(setf x nil y nil z nil)
ではなく,ただこうすればよい.
(nilf x y z)
最後のマクロtoggle
は前の節で説明した.
これはnilf
と似ているが,それぞれの引数に逆の真理値を与える.
これら4個のマクロは代入用オペレータに関する重要な点をはっきり表している.
オペレータを普通の変数のみに対して使うつもりでも,
setq
でなくsetf
に展開されるマクロを書いた方がよいということだ.
第1引数がシンボルならばsetf
は結局setq
に展開されるのだ.
setf
の一般性をなんの引替もなく享受できるのだから,
マクロ展開ではsetq
を使う方が好ましいことはまずないだろう.
(define-modify-macro concf (obj) nconc) (define-modify-macro conc1f (obj) (lambda (place obj) (nconc place (list obj)))) (define-modify-macro concnew (obj &rest args) (lambda (place obj &rest args) (unless (apply #'member obj place args) (nconc place (list obj)))))\caption{汎変数に対するリスト操作.} \label{fig:ListOpOnGnrlVars}
第\ref{fig:ListOpOnGnrlVars}図にはリスト末尾を破壊的に操作するマクロを3個示した. 第3.1節では副作用を期待して
(nconc x y)
を使うことは安全ではなく,代わりにこうしなければならないことに触れた.
(setq x (nconc x y))
この慣用法はconcf
の中に埋め込まれている.
更に特化したconc1f
とconcnew
とは,
それぞれpush
とpushnew
とをリストの逆方向に使うようなものだ.
conc1f
は1個の要素をリスト末尾に追加する.
concnew
は同様だが要素が既に含まれていないときに限る.
第2.2節で,関数名はシンボルの他にλ式であってもよいと書いた.
そのためconc1f
の定義のように,
λ式を丸ごとdefine-modify-macro
の第3引数に使っても結構だ.
ertページのconc1
を使えば,このマクロは次のようにも書ける.
(define-modify-macro conc1f (obj) conc1)
第\ref{fig:ListOpOnGnrlVars}図のマクロはある留保の元で使うべきだ.
すなわち,末尾に要素を追加していってリストを作ろうと思っているのなら,
push
を使い,最後にリストをnreverse
でインヴァージョンする方が好ましいという点だ.
リストの先頭に何か操作を行う方が,末尾に行うよりも楽に済む.
末尾に操作を行うには,まずそこまで到達しなければならないからだ.
Common Lispに前者を行うオペレータが多く,後者のためのものが少ないのは,
おそらく効率的なプログラミングを励行するためだろう.
define-modify-macro
だけでsetf
の上にどんなマクロも構築できるわけではない.
例えば,汎変数に破壊的に関数を適用するマクロf
を定義したいとしよう.
組込みマクロincf
は,+
にsetf
を使うことの省略だ.
次のようにしなくとも,
(setf x (+ x y))
ただこうすればよい.
(incf x y)
新しく作るf
はこの考えの一般化だ.
incf
は +
の呼び出しに展開されるが,
f
は第1引数として渡されたオペレータの呼び出しに展開される.
例えばcvbページのscale-objs
の定義では,こう書かなければならなかった.
(setf (obj-dx o) (* (obj-dx o) factor))
f
を使えば,これは次のようになる.
(_f * (obj-dx o) factor)
f
の定義で犯す誤りは,次のようなものだろう.
(defmacro _f (op place &rest args) ; 誤り `(setf ,place (,op ,place ,@args)))
残念だが,define-modify-macro
では適切にf
を定義することはできない.
それは汎変数に適用されるオペレータが引数として与えられるからだ.
このように更に複雑なマクロは手作りで定義しなければならない.
そういったマクロを書き易くするため,Common Lispは関数get-setf-method
を提供している.
これは汎変数を取り,その値を取得または設定するために必要な全ての情報を返すものだ.
次の式を手で展開することで,この情報の使い方を示す.
(incf (aref a (incf i)))
get-setf-method
を汎変数に対して呼び出すと,マクロ展開内での使うための値が5個返される.
& (get-setf-method '(aref a (incf i))) (#:G4 #:G5) (A (INCF I)) (#:G6) (SYSTEM:SET-AREF #:G6 #:G4 #:G5) (AREF #:G4 #:G5)
最初の2個の値は,一時変数とそれに代入するべき値のリストだ. だから展開形を次のように始めることができる.
(let* ((#:g4 a) (#:g5 (incf i))) ...)
一般的な状況では値の式は先に出てきた式を参照しているかも知れないので,
これらの束縛はlet*
内で作られるべきだ.
3番目と
\footnote{第3の値は現在のところ常に1要素から成るリストだ.
これがリストとして与えられるのは,
汎変数に複数の値を蓄える(これまでのところ実現されていない)可能性を提供するためだ.}
5番目の値は,別の一時変数と汎変数の元の値を返す式だ.
この値に1を加えたいので,後者を 1+
の呼び出し内に入れる.
(let* ((#:g4 a) (#:g5 (incf i)) (#:g6 (1+ (aref #:g4 #:g5)))) ...)
最後にget-setf-method
の返す4番目の値とは,
新しい束縛のスコープ内で行われなければならない代入操作だ.
(let* ((#:g4 a) (#:g5 (incf i)) (#:g6 (1+ (aref #:g4 #:g5)))) (system:set-aref #:g6 #:g4 #:g5))
この式はCommon Lispの一部ではない内部関数を参照していることの方が多い.
普通setf
はそれらの存在を隠蔽しているが,それらはどこかに存在していなければならない.
それに関することはみな実装依存なので,
可搬性を持たせたいコードではsystem:set-aref
のような関数を直接参照せず,
get-setf-method
の返した値を使ようにするべきだ.
さてf
を実装するためには,
incf
を手で展開するときに行ったこととほとんど同じことを行うマクロを書くことになる.
唯一の違いは,let*
内の最後の式を1+
の呼び出しの中に入れるのではなく,
f
の引数から作られた式の中に入れることだ.
f
の定義は第\ref{fig:MoreComplexMacrosOnSetf}図に示したようになる.
(defmacro _f (op place &rest args) (multiple-value-bind (vars forms var set access) (get-setf-method place) `(let* (,@(mapcar #'list vars forms) (,(car var) (,op ,access ,@args))) ,set))) (defmacro pull (obj place &rest args) (multiple-value-bind (vars forms var set access) (get-setf-method place) (let ((g (gensym))) `(let* ((,g ,obj) ,@(mapcar #'list vars forms) (,(car var) (delete ,g ,access ,@args))) ,set)))) (defmacro pull-if (test place &rest args) (multiple-value-bind (vars forms var set access) (get-setf-method place) (let ((g (gensym))) `(let* ((,g ,test) ,@(mapcar #'list vars forms) (,(car var) (delete-if ,g ,access ,@args))) ,set)))) (defmacro popn (n place) (multiple-value-bind (vars forms var set access) (get-setf-method place) (with-gensyms (gn glst) `(let* ((,gn ,n) ,@(mapcar #'list vars forms) (,glst ,access) (,(car var) (nthcdr ,gn ,glst))) (prog1 (subseq ,glst 0 ,gn) ,set)))))\caption{
setf
の上に作る更に複雑なマクロ.}
\label{fig:MoreComplexMacrosOnSetf}
このユーティリティはとても便利なものだ.
今これが手に入ったので,
例えば名前を持った関数ならメモワイズ技法(第5.3節)を適用したものと簡単に置き換えることができる
\footnote{しかし組込み関数にはこの方法でメモワイズ機能を付けるべきではない.
Common Lispは組込み関数の再定義を禁じている.}.
foo
をメモワイズ機能付きにするには,このようにすればよい.
(_f memoize (symbol-function 'foo))
f
を利用すると,setf
の上に他のマクロを定義することも簡単になる.
例えばconc1f
(第\ref{fig:ListOpOnGnrlVars}図)は次のように定義できる.
(defmacro conc1f (lst obj) `(_f nconc ,lst (list ,obj)))
第\ref{fig:MoreComplexMacrosOnSetf}図にはsetf
の上に構築した便利なマクロを他にも示した.
2番目のpull
は,組込み関数pushnew
のコンプリメント(complement)として意図されている.
これら2つは,push
とpop
が一層明確になったようなものだ.
pushnew
はオブジェクトが既にリストのメンバでないときだけそれをリストにプッシュし,
pull
は選択された要素をリストから破壊的に削除する.
pull
の定義内の&rest
パラメータにより,
pull
はdelete
と同じキーワード引数を受け付けるようになっている.
> (setq x '(1 2 (a b) 3)) (12(AB)3) > (pull 2 x) (1 (A B) 3) > (pull '(a b) x :test #'equal) (1 3) > x (1 3)
このマクロはあたかも次のように定義されたかのように考えられる.
(defmacro pull (obj seq &rest args) ; 誤り `(setf ,seq (delete ,obj ,seq ,@args)))
しかしこう定義すると評価順と評価回数の両方に関して問題が生じる.
単純にmodify-macro
で定義したpull
も作れる.
(define-modify-macro pull (obj &rest args) (lambda (seq obj &rest args) (apply #'delete obj seq args)))
しかしmodify-macros
は汎変数を第1引数として取らなければならないので,
第1,第2引数を逆順に与えなければならないが,これはやや直観に反する.
更に一般的なpull-if
は関数を引数に取り, delete
でなくdelete-if
に展開される.
> (let ((lst '(1 2 3 4 5 6))) (pull-if #'oddp lst) lst) (2 4 6)
これら2つのマクロからは,一般的なポイントが他にも分かる.
基盤となる関数がオプショナル引数をとるときは,その上に構築されたマクロもそうなるべきだという点だ.
pull
とpull-if
は両方ともオプショナル引数を内部のdelete
に渡すようになっている.
第\ref{fig:MoreComplexMacrosOnSetf}図の最後のマクロpopn
は,pop
の一般化だ.
リストに1要素だけをポップするのではなく,任意の部分リストをポップして返す.
> (setq x '(a b c d e f)) (A B C D E F) > (popn 3 x) (A B C) > x (D E F)
第\ref{fig:MacroWhichSortsArgs}には引数を整列させるマクロを示した.
x
とy
が変数で,x
の値の方が小さくはないようにしたいときは,こうすればよい.
(if (> y x) (rotatef x y))
しかしこれを3個以上の変数について行おうとすると,コードはたちまち肥大化する.
これを手で書く代わりに,sortf
に書かせることができる.
このマクロは比較オペレータと任意個の汎変数を取り,
それらの値がオペレータによって指定された順に並ぶまで値を入れ換える.
一番単純な場合では,引数は普通の変数になる.
> (setq x 1 y 2 z 3) 3 > (sortf > x y z) 3 > (list x y z) (3 2 1)
(defmacro sortf (op &rest places) (let* ((meths (mapcar #'(lambda (p) (multiple-value-list (get-setf-method p))) places)) (temps (apply #'append (mapcar #'third meths)))) `(let* ,(mapcar #'list (mapcan #'(lambda (m) (append (first m) (third m))) meths) (mapcan #'(lambda (m) (append (second m) (list (fifth m)))) meths)) ,@(mapcon #'(lambda (rest) (mapcar #'(lambda (arg) `(unless (,op ,(car rest) ,arg) (rotatef ,(car rest) ,arg))) (cdr rest}))) temps) ,@(mapcar #'fourth meths))))\caption{引数を整列させるマクロ.} \label{fig:MacroWhichSortsArgs}
一般的には,それらは任意のインヴァージョン可能な式であってよい.
ここでcake
が誰かのケーキの取り分を返すインヴァージョン可能な関数であり,
bigger
がケーキに関して定義された比較関数だとしよう.
moe
のケーキは少なくともlarry
のケーキ程はあり,
それは少なくともcurly
のケーキ程はあるという規則を立てたいならば,こうすればよい.
(sortf bigger (cake 'moe) (cake 'larry) (cake 'curly))
sortf
の定義は,概形としてはf
の定義に似ている.
最初にlet*
があり,get-setf-method
の返した一時変数が汎変数の初期値に束縛される.
sortf
の中核は真中の式mapcon
であり,これがその一時変数を整列させるためのコードを生成する.
マクロのこの部分が生成するコードは引数の数に関して指数的に増大する.
整列の後,get-setf-method
の返した式を使って汎変数に再び値が代入される.
使われているアルゴリズムは$O(n^2)$オーダのバブル・ソートだが,
このマクロは大量の引数について呼ばれることを意図したものではない.
(sortf > x (aref ar (incf i)) (car lst))は(ある処理系では)次のように展開される:
(let* ((#:g1 x) (#:g4 ar) (#:g3 (incf i)) (#:g2 (aref #:g4 #:g3)) (#:g6 lst) (#:g5 (car #:g6))) (unless (> #:g1 #:g2) (rotatef #:g1 #:g2)) (unless (> #:g1 #:g5) (rotatef #:g1 #:g5)) (unless (> #:g2 #:g5) (rotatef #:g2 #:g5)) (setq x #:g1) (system:set-aref #:g2 #:g4 #:g3) (system:set-car #:g6 #:g5))\caption{
sortf
の呼び出しの展開形.}
\label{fig:ExpansionOfSortf}
第\ref{fig:ExpansionOfSortf}図には,sortf
の呼び出しの展開形を示した.
最初のlet*
で,引数とその部分式が注意深く左から右の順に評価される.
そしてその次の3個の式で一時変数の値を比較し,場合によっては値を入れ換える.
第1の一時変数が第2と比較され,次に第1と第3が比較され,次に第2と第3が比較される.
最後に汎変数は左から右の順に再代入を受ける.
問題になることは滅多にないが,マクロの引数は評価と同様に普通は左から右の順で値を代入されるべきだ.
f
やsortf
等のオペレータには,関数の引数を取る関数とのある種の類似性がある.
しかしこれらは全く違ったものとして理解されるべきだ.
find-if
のような関数は関数を取ってそれを呼び出す.
f
のようなマクロは関数の名前を取り,それを式のcar部に置く.
f
とsortf
は共に関数の引数を取るように書くこともできた.
例えばf
を次のように書いて,
(defmacro _f (op place &rest args) (let ((g (gensym))) (multiple-value-bind (vars forms var set access) (get-setf-method place) `(let* ((,g ,op) ,@(mapcar #'list vars forms) (,(car var) (funcall ,g ,access ,@args))) ,set))))
(f #'+ x 1)
として呼び出すこともできた.
しかし元のf
はこれができることを全てできるし,扱うものが名前なのでマクロや特殊式の名前も受け付ける.
+
の他にも,例えばnif
(rtyページ)も使える.
> (let ((x 2)) (_f nif x 'p 'z 'n) x) P
第12.1節ではインヴァージョン可能な参照に展開される任意のマクロ呼び出しは,
それ自身がインヴァージョン可能だということを説明した.
しかし,オペレータをインヴァージョン可能にするだけのためにそれをマクロとして定義する必要はない.
defsetf
を使えば,Lispに任意の関数やマクロ呼び出しをインヴァージョンする方法を教えることができる.
このマクロの使い方は2通りある. 一番単純な場合では,引数は2個のシンボルになる.
(defsetf symbol-value set)
複雑な形では,defsetf
の呼び出しはdefmacro
の呼び出しと似たようなものになり,
更新された値の式のための仮引数が加わる.
例えば,次のようにしてcar
に対するインヴァージョンを定義できる.
(defsetf car (lst) (new-car) `(progn (rplaca ,lst ,new-car) ,new-car))
defmacro
とdefsetf
には,重要な違いが1個ある.
後者は引数のために自動的にgensymを生成する点だ.
上の定義の下では,(setf (car x) y)
は次のように展開されるだろう.
(let* ((#:g2 x) (#:g1 y)) (progn (rplaca #:g2 #:g1) #:g1))
そのため変数捕捉及び評価の回数や順番に煩わされずにdefsetf
の展開関数を書くことができる.
(defvar *cache* (make-hash-table)) (defun retrieve (key) (multiple-value-bind (x y) (gethash key *cache*) (if y(values x y) (cdr (assoc key *world*))))) (defsetf retrieve (key) (val) `(setf (gethash ,key *cache*) ,val))\caption{非対称なインヴァージョン.} \label{fig:AsymmetricInversion}
CLtL2準拠のCommon Lispでは,
setf
に対するインヴァージョンをdefun
で直接定義できる.
よって上の例は次のようにも書ける.
(defun (setf car) (new-car lst) (rplaca lst new-car) new-car)
このような関数では,更新された値が第1引数になるべきだ. また,この値を関数の値として返すことも慣習になっている.
これまでの例では汎変数とはデータ構造内の場所を参照するためのものかのように示唆してきた.
悪者が人質を地下迷宮の中に連れ去ると,彼女を救うヒーローは外に連れ出すことになる.
悪者もヒーローも同じ道を通るが,その方向は逆だ.
setf
がこのように動作しなければならないという印象を持たれても驚きはしない.
定義済みのインヴァージョンは全てこの形式になっているようだからだ.
実際は,「場所」はインヴァージョンを受ける仮引数に対する慣用名に過ぎない.
原則的には,setf
はもっと一般的なものだ.
アクセス用の式とそのインヴァージョンは同じデータ構造に対して働く必要すらない.
何かのアプリケーションで,データベースの更新をキャッシュしたいとしよう.
これが必要になるのは,例えば動作中に実際に更新を行うのが非効率的だったり,
一貫性を保つために更新点をコミット前に検証しなければならないときだ.
*world*
が実際のデータベースだとしよう.
単純化のため,これはalistで,
その要素は(
<キー> .
<値> )
という形式だとする.
第\ref{fig:AsymmetricInversion}図にはretrieve
という名前の検索関数を示した.
*world*
が次の状態のときは,
((a . 2) (b . 16) (c . 50) (d . 20) (f . 12))
次のように動作する.
> (retrieve 'c) 50
car
の呼び出しと異なり,
retrieve
の呼び出しはデータ構造の特定の場所を参照しているのではない.
返り値は2箇所の場所のうちどちらかから得られることになる.
そしてretrieve
のインヴァージョン(やはり第\ref{fig:AsymmetricInversion}図に示した)は,
その片方を参照するだけだ.
> (setf (retrieve 'n) 77) 77 > (retrieve 'n) 77 T
この検索では第2の返り値t
が返るが,これは求める値がキャッシュ内に見つかったことを表す.
マクロそのものと同様に,汎変数も目覚しい力を持った抽象化手段だ.
その真価はここで語ったことに留まらないだろう.
個々の現場では汎変数の使用がエレガントで強力なプログラムにつながるような方法の方が
発見し易いことは確かだ.
しかしsetf
のインヴァージョンを新しい方法で使ったり,
同様に便利な変換手法を発見することも可能かも知れない.