汎変数

第8章では,マクロの利点の1つは引数を変形できる能力だということに触れた. その種のマクロの1つがsetfだ. この章では,setfの持つ隠れた意味に視点を向け,その上に構築できるマクロの例を幾つか示す.

setfの上に適切なマクロを構築することは,驚く程難しい. この話題への導入として,最初の節では少々まずい所のある単純な例を示す. 次の節ではそれの何がまずいのかを説明し,改善方法を示す. その後の2節ではsetfの上に構築されたユーティリティの例を提示し, 最後の節でsetfの逆に当たる独自のオペレータの定義方法を説明する.

概念

組込みマクロsetfsetqの一般化に当たる. setfの第1引数はただの変数でなく関数やマクロの呼び出しであってもよい.

> (setq lst '(a b c))
(ABC)
> (setf (car lst) 480)
480
> lst
(480 B C)

一般的には(setf x y)は 「xyに評価されるようにしておいてくれ」という指示として理解できる. setfはマクロなので,引数の内部を見てそのような指示を実現するには何をすればよいかを判断できる. (展開後の)1引数がシンボルだったら,setfは単にsetqに展開される. しかし第1引数が別の変数を求めるための式だったときは, setfは対応する確定した命令(assertion)に展開される. 第2引数は定数なので,前の例は次のように展開されるかもしれない.

(progn (rplaca lst 480) 480)

このように変数を求める式から決定的な命令へ変換することはインヴァージョン(inversion)と呼ばれる. carcdrntharefgetgethash及び 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つのグループに分けられる. グループというものはいつでも互いに「仲間じゃないやつは敵だ」と言いたがるので, 町の人間はどちらかのグループに属することを余儀なくされる. だから誰かがグループを移ると,元の友人は敵に,敵は友人に変わる.

組込みオペレータだけによってxyの友人かどうかを切替えるには, 次のようにすることになる.

(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)

この定義は正しい結果をもたらすが,更に一般化することもできる. setfsetqは任意の数の引数を取れるのだから,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は,複数の汎変数を同じ値に設定するためのものだ. さらにその上にnilftfが作られた. これらは引数をそれぞれniltに設定する. これらは単純なマクロだが,プログラムに大きな違いを生む.

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の中に埋め込まれている. 更に特化したconc1fconcnewとは, それぞれpushpushnewとをリストの逆方向に使うようなものだ. 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つは,pushpopが一層明確になったようなものだ. pushnewはオブジェクトが既にリストのメンバでないときだけそれをリストにプッシュし, pullは選択された要素をリストから破壊的に削除する. pullの定義内の&restパラメータにより, pulldeleteと同じキーワード引数を受け付けるようになっている.

> (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つのマクロからは,一般的なポイントが他にも分かる. 基盤となる関数がオプショナル引数をとるときは,その上に構築されたマクロもそうなるべきだという点だ. pullpull-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}には引数を整列させるマクロを示した. xyが変数で,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が比較される. 最後に汎変数は左から右の順に再代入を受ける. 問題になることは滅多にないが,マクロの引数は評価と同様に普通は左から右の順で値を代入されるべきだ.

fsortf等のオペレータには,関数の引数を取る関数とのある種の類似性がある. しかしこれらは全く違ったものとして理解されるべきだ. find-ifのような関数は関数を取ってそれを呼び出す. fのようなマクロは関数の名前を取り,それを式のcar部に置く. fsortfは共に関数の引数を取るように書くこともできた. 例えば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))

defmacrodefsetfには,重要な違いが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のインヴァージョンを新しい方法で使ったり, 同様に便利な変換手法を発見することも可能かも知れない.


←: 古典的なマクロ     ↑: On Lisp     →: コンパイル時の計算処理

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