第9章では,変数捕捉をどれも問題として扱った ---うっかりすると起きてしまい,プログラムに悪い影響だけを与えるものとして. この章では変数捕捉は建設的にも使えることを示す. それなしでは書けないような便利なマクロが幾つかあるのだ.
ある式が非nil
の値を返すかどうか調べ,
そうならば値に何かを行いたいようなことはLispプログラミングでは珍しくない.
式の評価に大きな負担がかかるならば,普通は次のようにしなければならないだろう.
(let ((result (big-long-calculation))) (if result (foo result)))
日常言語と同じように,これを次のように表現できたら楽ではないだろうか?
(if (big-long-calculation) (foo it))
変数捕捉を利用することで,まさにこのとおりに動作するif
の変種を定義できる.
日常言語では,アナフォラ(前方照応,anaphora)とは会話の中で前に出てきたことを指す表現のことだ.
英語で一番馴染み深いアナフォラは恐らく「それ」で,
「レンチを取ったら,それを机に置いてくれ」等と使われる
(訳注:日本語ではそもそも省かれるので例が不自然になってしまった).
アナフォラは日常会話の中で大変便利なものだ ---それを使わないことを想像してみよう---
しかしそれはプログラミング言語には出てこない
(訳注:PerlやRubyでは $_
で「最後に読み込んだ文字列」が表せる).
ほとんどの局面では,それでいい.
アナフォリックな(前方照応的な,anaphoric)表現はしばしばひどく曖昧だが,
現代のプログラミング言語は曖昧性を扱うようには設計されていないからだ.
しかし非常に制限された形式のアナフォラを,曖昧さを入れずにLispプログラムに導入することは可能だ. アナフォラは捕捉されたシンボルにそっくりだということが分かる. 代名詞として働くシンボルを作り,そしてそれらのシンボルを捕捉するマクロを意図的に書くことで, アナフォラをプログラムに導入できる.
新型のif
では,シンボルit
が捕捉したいものだ.
アナフォリックなif
は簡潔にaif
と呼ぶことにするが,次のように定義され,
(defmacro aif (test-form then-form &optional else-form) `(let ((it ,test-form)) (if it ,then-form ,else-form)))
前の方の例と同じように使われる.
(aif (big-long-calculation) (foo it))
aif
を使うときは,テスト節の返す結果にシンボルit
が束縛されたままになる.
上のマクロ呼び出しではit
はフリーのように見えるが,
実際は式(foo it)
はaif
の展開に伴い,
シンボルit
に束縛のあるコード内に挿入される.
(let ((it (big-long-calculation))) (if it (foo it) nil))
よってソースコード内ではフリーに見えるシンボルはマクロ展開によって束縛されたままになる. この章で示すアナフォリックマクロは,全て同じ手法に基づくものだ.
(defmacro aif (test-form then-form &optional else-form) `(let ((it ,test-form)) (if it ,then-form ,else-form))) (defmacro awhen (test-form &body body) `(aif ,test-form (progn ,@body))) (defmacro awhile (expr &body body) `(do ((it ,expr ,expr)) ((not it)) ,@body)) (defmacro aand (&rest args) (cond ((null args) t) ((null (cdr args)) (car args)) (t `(aif ,(car args) (aand ,@(cdr args)))))) (defmacro acond (&rest clauses) (if (null clauses) nil (let ((cl1 (car clauses)) (sym (gensym))) `(let ((,sym ,(car cl1))) (if ,sym (let ((it ,sym)) ,@(cdr cl1)) (acond ,@(cdr clauses)))))))\caption{Common Lispの標準オペレータのアナフォリックな変種.} \label{fig:AnaphoricVariants}
第\ref{fig:AnaphoricVariants}図には幾つかのCommon Lispオペレータのアナフォリックな変種を示した.
aif
の次のawhen
は,when
をアナフォリックにしたものだ.
(awhen (big-long-calculation) (foo it) (bar it))
aif
もawhen
も頻繁に役立つが,
awhile
はアナフォリックでないwhile
(vgpページで定義された)より多く必要になるという点で
アナフォリックマクロの中では恐らくユニークなものだろう.
while
とawhile
のようなマクロは,
プログラムが外部データを監視する必要がある状況で使われるのが典型的だ.
そしてデータの監視を繰り返す間,それが状態を変化させるのをただ待っているのでなければ,
普通は得られたオブジェクトに対し何かを行いたいことだろう.
(awhile (poll *fridge*) (eat it))
aand
の定義はそれまでのものより少々複雑になっている.
これはアナフォリックなand
を与える.
それぞれの引数の評価の間,it
はその前の引数が返した値に束縛される
\footnote{and
とor
は一緒にして考えられがちだが,
アナフォリックなor
を書いても意味はないだろう.
or
の引数が評価されるのは,それより前の引数がnil
に評価されたときだけだ.
だからaor
の中でアナフォリックシンボルを参照しても,何も便利なことはないだろう.}.
実用では,aand
は条件に基づいてクエリを発行するプログラムで使われることが多い.
(aand (owner x) (address it) (town it))
これはx
の所有者の住所(所有者がいるならば)のある町(住所があるならば)
を返す(町があるならば).
aand
なしでは,これは次のように書かなければならないだろう.
(let ((own (owner x))) (if own (let ((adr (address own))) (if adr (town adr)))))
aand
の定義は,展開形がマクロ呼び出しの引数の数によって変わることを示している.
引数が0個のときは,aand
は普通のand
と同様にただt
を返すだけだ.
そうでなければ展開形は再帰的に生成される.
再帰の段階毎に入れ子になったaif
の連鎖に新しい層が追加される.
(aif <第1引数> <引数の残り> )
大部分の再帰関数がnil
を受け取る段階まで再帰を続けるのとは違い,
aand
の展開は引数が1個の時点で終結しなければならない.
選択肢が1個もない段階まで再帰が進み過ぎると,展開形は必ず次のような形になる.
(aif c1 ...(aif cn t)...)
このような式は必ずt
かnil
のどちらかを返すので,
上の例が意図した通りに動作しなくなる.
第10.4節で,マクロが常に自分の呼び出しを含む展開形を生成するようでは,
展開は永遠に終結しないと警告した.
aand
も再帰的だが,ベース・ケースでは展開形はaand
を参照しないので安全だ.
最後のacond
は,
cond
の節のテスト式以外でテスト式の返した値を使いたい場合のために作られた.
(この状況はとてもよく現れるので,
Schemeの処理系によってはcond
の節の中でテスト式の返した値を使う方法を提供している.)
(defmacro alambda (parms &body body) `(labels ((self ,parms ,@body)) #'self)) (defmacro ablock (tag &rest args) `(block ,tag ,(funcall (alambda (args) (case (length args) (0 nil) (1 (car args)) (t `(let ((it ,(car args))) ,(self (cdr args)))))) args)))\caption{アナフォリックなオペレータの続き.} \label{fig:MoreAnaphoricVariants}
acond
の節の展開形の中では,テスト式の結果は最初にgensymに保持される.
これはシンボルit
が節の残りの部分でのみ束縛されるようにするためだ.
マクロが束縛を生成するときには,必ず可能なかぎり狭いスコープの中で生成するべきだ.
ここでgensymをけちって,代わりに次のようにit
をテスト式の結果に直接束縛すると,
(defmacro acond (&rest clauses) ; 誤り (if (null clauses) nil (let ((cl1 (car clauses))) `(let ((it ,(car cl1))) (if it (progn ,@(cdr cl1)) (acond ,@(cdr clauses)))))))
it
の束縛はその次のテスト式もスコープに含んでしまうだろう.
第\ref{fig:MoreAnaphoricVariants}図には標準オペレータのアナフォリックな変種の,
更に複雑なものを示した.
マクロalambda
は書かれているままの再帰関数を参照するためのものだ.
書かれているままの再帰関数を参照したいときとはいつだろうか?
シャープクォート付きλ式を使えば書かれているままの関数にアクセスできる.
#'(lambda (x) (* x 2))
しかし第2章で説明したように,単純なλ式だけでは再帰関数は表現できない.
代わりにlabels
でローカル関数を定義しなければならない.
次の関数(ertページから再録)は,
(defun count-instances (obj lists) (labels ((instances-in (list) (if list (+ (if (eq (car list) obj) 1 0) (instances-in (cdr list))) 0))) (mapcar #'instances-in lists)))
オブジェクトとリストを引数に取り,リストの要素毎にそのオブジェクトが幾つ現れたかを返す.
> (count-instances 'a '((a b c) (d a r p a) (d a r) (a a))) (1 2 1 2)
アナフォラを使えば書かれているままの再帰関数に相当するものが作れる.
マクロalambda
はlabels
を使ってそのようなものを作るので,
例えば階乗関数を表現するのに使える.
(alambda (x) (if (= x 0) 1 (* x (self (1- x)))))
alambda
を使ってcount-instances
と等価なものが次のように定義できる.
(defun count-instances (obj lists) (mapcar (alambda (list) (if list (+ (if (eq (car list) obj) 1 0) (self (cdr list))) 0)) lists))
第\ref{fig:MoreAnaphoricVariants},\ref{fig:AnaphoricVariants}図の
他のマクロは全てit
を捕捉するが,それらと異なり,
alambda
はself
を捕捉する.
alambda
はlabels
に展開されるが,
その中ではself
は定義されている関数自身に束縛されている.
alambda
は短いだけでなく,見慣れたλ式と似ており,
それを使っているコードを読み易くしている.
ablock
の定義では新しいマクロが使われている.
組み込み特殊式block
のアナフォリック版だ.
block
では引数は全て左から右の順に評価される.
ablock
でもそれは同じだが,それぞれの引数において変数it
は前の引数の値に束縛される.
このマクロは思慮を持って使うべきだ.
便利なこともあるが,ablock
はうまく関数的になれるはずのプログラムを命令的に変えてしまいがちだ.
次のコードは,残念ながら典型的な汚い例だ.
> (ablock north-pole (princ "ho ") (princ it) (princ it) (return-from north-pole)) ho ho ho NIL
意図的に変数捕捉を行うマクロが別のパッケージにエクスポートされるときにはいつでも,
捕捉されるシンボルもエクスポートすることが必要だ.
例えばaif
がエクスポートされるときにはいつでもit
もそうしなければならない.
そうしないとマクロ定義内のit
は
マクロ呼び出し内で使われているit
とは別のシンボルになってしまうだろう.
Common Lispではシンボルnil
には少なくとも3種類の役割がある.
まずなにより空リストであって,次のように働く.
> (cdr '(a)) NIL
空リストの他に,nil
は次のように真理値の偽の表現にも使われる.
> (= 1 0) NIL
そして最後に,関数はnil
を返すことで失敗を表す.
例えば組み込み関数find-if
はリストの要素で何かの条件を満たす最初のものを返す.
そのような要素が見付からなければ,find-if
はnil
を返す.
> (find-if #'oddp '(2 4 6)) NIL
残念なことに上の場合は,find-if
が検索に成功したが,
検索したものがnil
だった場合と区別がつかない.
> (find-if #'null '(2 nil 6)) NIL
nil
で偽と空リストの両方を表現したところで,実用的には大きな問題は起きない.
実際,むしろ便利なこともある.
しかしfind-if
のような関数の返した結果に曖昧性が生まれるので,
nil
で失敗も表現するのは困ったことだ.
失敗と返り値のnil
を区別する問題は,オブジェクトを検索する関数全てで現れる.
Common Lispはこの問題に3通りもの解決策を用意している.
(多値を返す方法の次に)最もよく使われる方法は,とりあえずリスト構造を返しておくことだ.
例えばassoc
で検索失敗を見分けるのには何の問題もない.
成功したときにはassoc
はキーと値の対全体を返すからだ.
> (setq synonyms '((yes . t) (no . nil))) ((YES . T) (NO)) > (assoc 'no synonyms) (NO)
この方法に倣い,find-if
の曖昧性が心配なときは,member-if
を使うことにする.
これは条件を満たす要素だけを返すのではなく,それから始まるcdr
部全体を返す.
> (member-if #'null '(2 nil 6)) (NIL 6)
多値が利用可能になって以来,この問題には新しい解決策が加わった.
第1返り値をデータに,第2返り値を成功/失敗の表現に使うのだ.
組み込み関数gethash
はそのように動作する.
これは常に2個の値を返すが,2番目は何かが見つかったかどうかを示すものだ.
> (setf edible (make-hash-table) (gethash 'olive-oil edible) t (gethash 'motor-oil edible) nil) NIL > (gethash 'motor-oil edible) NIL T
だから起こり得る3通りの場合を全て判別したいなら,次のような慣用法が使える.
(defun edible? (x) (multiple-value-bind (val found?) (gethash x edible) (if found? (if val 'yes 'no) 'maybe)))
これによれば偽を失敗と区別するには次のようにすればよい.
> (mapcar #'edible? '(motor-oil olive-oil iguana)) (NO YES MAYBE)
Common Lispでは失敗を表すのに3番目の方法が利用できる.
アクセス関数が,失敗のときに返すための特別なオブジェクト
(おそらくgensymを使うことになるだろう)を引数に取るようにすることだ.
この手法が使われているget
はオプショナルな引数を取り,
指定された属性が見つからなかったときにはそれを返す.
> (get 'life 'meaning (gensym)) #:G618
多値を返せるときには,gethash
で使われている手法が一番きれいだ.
get
でしているように,
全てのアクセス関数にオプショナル引数を渡さなければならないようなことは避けたい.
そしてリストを使う手法と比べても,多値を使う方が一般性が高い.
find-if
は2個の値を返すように書き換えられるが,
gethash
はコンシングを起こさずに曖昧さを無くせるようなリスト構造を返すように
書き換えることはできない.
よって検索用の新関数を書くときや,失敗が起こり得る操作のためには,
普通はgethash
の手法に倣った方がよい.
(defmacro aif2 (test &optional then else) (let ((win (gensym))) `(multiple-value-bind (it ,win) ,test (if (or it ,win) ,then ,else)))) (defmacro awhen2 (test &body body) `(aif2 ,test (progn ,@body))) (defmacro awhile2 (test &body body) (let ((flag (gensym))) `(let ((,flag t)) (while ,flag (aif2 ,test (progn ,@body) (setq ,flag nil)))))) (defmacro acond2 (&rest clauses) (if (null clauses) nil (let ((cl1 (car clauses)) (val (gensym)) (win (gensym))) `(multiple-value-bind (,val ,win) ,(car cl1) (if (or ,val ,win) (let ((it ,val)) ,@(cdr cl1)) (acond2 ,@(cdr clauses)))))))\caption{多値を返すアナフォリックマクロ.} \label{fig:Multiple-valueAnaphoricMacros}
edible?
で使われている慣用法は正にマクロで隠蔽できる類の事務的処理だ.
gethash
等のアクセス関数に対しては,
同じ式の真偽を調べて結果を束縛するのでなく,
第1引数を調べて第2引数を束縛するようにaif
を改変したものが欲しくなる.
新しいaif
をaif2
と名付けるが,
それを第\ref{fig:Multiple-valueAnaphoricMacros}図に示した.
これを使えばedible?
は次のように書ける.
(defun edible? (x) (aif2 (gethash x edible) (if it 'yes 'no) 'maybe))
第\ref{fig:Multiple-valueAnaphoricMacros}図にはawhen
,
awhile
やacond
を同様に改変したものも示した.
acond2
の用例についてはeyoiページのmatch
の定義を参照.
このマクロなしでは遥かに長くなって対称性もなくなってしまう関数も,
このマクロを使えばcond
の形で表現できるようになる.
(let ((g (gensym))) (defun read2 (&optional (str *standard-input*)) (let ((val (read str nil g))) (unless (equal val g) (values val t))))) (defmacro do-file (filename &body body) (let ((str (gensym))) `(with-open-file (,str ,filename) (awhile2 (read2 ,str) ,@body))))\caption{ファイル用ユーティリティ.} \label{fig:FileUtilities}
組み込み関数read
はget
と同じ方法で失敗を表す.
これが取るオプショナル引数はeof
に出会ったときにエラーを起こすかどうか指定するものと,
起こさないときに返す値を指定するものだ.
第\ref{fig:FileUtilities}図に示したread
の別の定義は,第2返り値を使って失敗を表す.
read2
はが返す2個の値は,入力された式と,eof
のときにnil
になるフラグだ.
これはeof
のときに返されるgensym等を引数にread
を呼ぶが,
read2
が呼ばれる度にgensymを生成する負担を節約するため,
コンパイル時に生成されるgensymの独自コピーを持ったクロージャとして定義されている.
第\ref{fig:FileUtilities}図には,ファイル内の式について反復を行うための便利なマクロも示した.
これはawhile2
とread2
を使って定義されている.
do-file
を使うことで,例えば一種のload
を次のように定義できる.
(defun our-load (filename) (do-file filename (eval it)))
アナフォリックマクロは参照透明性を侵すと言われることがある. GelernterとJagannathanは参照透明性を次のように定義した.
参照の透明性が保たれているプログラミング言語とは,\\ a) どの部分式も,値の等しい別の式に置き換えることができ,\\ b) 式は任意のコンテキスト内のどこで何回使われても同じ値を返す,ようなものだ.
この基準はプログラミング言語に適用されるもので,プログラムには適用されないことに注意しよう.
代入操作を持つプログラミング言語はいずれも参照透明性を持たない.
次の式で,最初と最後のx
は同じ値を持たない.
(list x (setq x (not x)) x)
これはsetq
が介在しているからだ.
明らかに,これは汚いコードだ.
そのようなコードが可能であるという事実は,Lispは参照透明性を持たないということだ.
Norvigは,if
を次のように再定義したら便利だろうと述べたが,
(defmacro if (test then &optional else) `(let ((that ,test)) (if that ,then ,else)))
参照透明性を侵すという観点からこのマクロを却下している.
しかし問題は組み込みオペレータを再定義したことに因るもので,アナフォラを使ったことに因るのではない.
上の定義のb)節は,式が「任意のコンテキスト内」で必ず同じ値を返すことを要求している.
次のlet
の中では,シンボルthat
が新しい変数を表しても問題はない.
(let ((that 'which)) ...)
let
は当然新しいコンテキストを生成するはずのものだからだ.
上のマクロの問題はif
を再定義していることで,
そうすると新しいコンテキストが作られることにはならない.
アナフォリックマクロに別の名前を与えれば問題は消失する.
(それ以前に,CLtL2にあるようにif
の再定義は違法だ.)
そのようなマクロがaif
の定義の一部であることにより,
そのマクロが生成したコンテキスト内ではit
が新しい変数である限り,
そのマクロは参照透明性を侵さない.
さてaif
は確かに別の慣習に違反するが,それは参照透明性とは関係ない.
その慣習とは,新しく生成された変数は
ソースコード内で何らかの形で分かるようになっているべきというものだ.
上のlet
はthat
が新しい変数を参照するようになることをはっきり示している.
aif
内でのit
の束縛がはっきり見えないという議論もあるかも知れない.
しかしこれは余り説得力のある意見ではない.
aif
は1個の変数を生成するだけだし,
しかもその変数を生成することだけがaif
を使う理由だからだ.
Common Lispそのものはこの慣習を不可侵なものとはしていない.
CLOS関数call-next-method
の束縛はコンテキストに依存するが,
これはaif
の本体内でシンボルit
がそうなのとと全く同様だ.
(call-next-method
をどのように実装するかについてのアイディアについては,
xwgページのマクロdefmeth
を参照.)
どの場合にしろ,そういった慣習はある目的のための手段と考えられているに過ぎない.
読み易いプログラムという目的だ.
そしてアナフォラは,英語を読み易くしてくれるのと全く同様に,確かにプログラムを読み易くしてくれる.