アナフォリックマクロ

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

aifawhenも頻繁に役立つが, awhileはアナフォリックでないwhile(vgpページで定義された)より多く必要になるという点で アナフォリックマクロの中では恐らくユニークなものだろう. whileawhileのようなマクロは, プログラムが外部データを監視する必要がある状況で使われるのが典型的だ. そしてデータの監視を繰り返す間,それが状態を変化させるのをただ待っているのでなければ, 普通は得られたオブジェクトに対し何かを行いたいことだろう.

(awhile (poll *fridge*)
  (eat it))

aandの定義はそれまでのものより少々複雑になっている. これはアナフォリックなandを与える. それぞれの引数の評価の間,itはその前の引数が返した値に束縛される \footnote{andorは一緒にして考えられがちだが, アナフォリックな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)...)

このような式は必ずtnilのどちらかを返すので, 上の例が意図した通りに動作しなくなる.

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

アナフォラを使えば書かれているままの再帰関数に相当するものが作れる. マクロalambdalabelsを使ってそのようなものを作るので, 例えば階乗関数を表現するのに使える.

(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を捕捉するが,それらと異なり, alambdaselfを捕捉する. alambdalabelsに展開されるが, その中では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-ifnilを返す.

> (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を改変したものが欲しくなる. 新しいaifaif2と名付けるが, それを第\ref{fig:Multiple-valueAnaphoricMacros}図に示した. これを使えばedible?は次のように書ける.

(defun edible? (x)
  (aif2 (gethash x edible)
           (if it 'yes 'no)
           'maybe))

第\ref{fig:Multiple-valueAnaphoricMacros}図にはawhenawhileacondを同様に改変したものも示した. 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}

組み込み関数readgetと同じ方法で失敗を表す. これが取るオプショナル引数はeofに出会ったときにエラーを起こすかどうか指定するものと, 起こさないときに返す値を指定するものだ. 第\ref{fig:FileUtilities}図に示したreadの別の定義は,第2返り値を使って失敗を表す. read2はが返す2個の値は,入力された式と,eofのときにnilになるフラグだ. これはeofのときに返されるgensym等を引数にreadを呼ぶが, read2が呼ばれる度にgensymを生成する負担を節約するため, コンパイル時に生成されるgensymの独自コピーを持ったクロージャとして定義されている.

第\ref{fig:FileUtilities}図には,ファイル内の式について反復を行うための便利なマクロも示した. これはawhile2read2を使って定義されている. 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は確かに別の慣習に違反するが,それは参照透明性とは関係ない. その慣習とは,新しく生成された変数は ソースコード内で何らかの形で分かるようになっているべきというものだ. 上のletthatが新しい変数を参照するようになることをはっきり示している. aif内でのitの束縛がはっきり見えないという議論もあるかも知れない. しかしこれは余り説得力のある意見ではない. aifは1個の変数を生成するだけだし, しかもその変数を生成することだけがaifを使う理由だからだ.

Common Lispそのものはこの慣習を不可侵なものとはしていない. CLOS関数call-next-methodの束縛はコンテキストに依存するが, これはaifの本体内でシンボルitがそうなのとと全く同様だ. (call-next-methodをどのように実装するかについてのアイディアについては, xwgページのマクロdefmethを参照.) どの場合にしろ,そういった慣習はある目的のための手段と考えられているに過ぎない. 読み易いプログラムという目的だ. そしてアナフォラは,英語を読み易くしてくれるのと全く同様に,確かにプログラムを読み易くしてくれる.


←: コンパイル時の計算処理     ↑: On Lisp     →: 関数を返すマクロ

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