構造化代入とは,代入の一般化だ.
オペレータsetq
とsetf
は個々の変数に代入を行う.
構造化代入は代入をアクセスと組み合わせる.
第1引数に変数を単独で置くのではなく,変数のパターンを与える.
すると各々に同じ構造の対応する場所に置かれた値が代入される.
CLtL2では,Common Lispに新マクロdestructuring-bind
が加わった.
このマクロは第7章で簡単に扱ったが,ここでは詳しく検討する.
lst
が要素を3つ持つリストで,x
に第1要素を,y
に第2,
z
に第3要素を束縛したいとしよう.
CLtL1のみに対応のCommon Lispでは,こうしなければならない.
(let ((x (first lst)) (y (second lst)) (z (third lst))) ...)
新マクロを使うと,代わりにこう書くことができる.
(destructuring-bind (x y z) lst ...)
これは短いだけでなく,形も整っている.
ものを読むときには視覚的手がかりは文字上のものより遥かに速く認識される.
後の形ではx
,y
とz
の関係が示されているが,
前の形では推論が必要になる.
このように単純な場合すら構造化代入によってきれいにできるのだから,
複雑な場合にどれほど読みやすくなるか想像して欲しい.
destructuring-bind
の第1引数には任意の複雑なツリーを渡せる.
次のコードが,let
とリストへのアクセス関数を使って書かれたらどうなるか想像して欲しい.
(destructuring-bind ((first last) (month day year) . notes) birthday ...)
これから新しく分かることがある. 構造化代入は,プログラムを読み易くするだけでなく書き易くもする.
構造化代入はCLtL1のCommon Lispにも確かに存在した.
上述の例が見慣れたものに思えるなら,それはマクロの仮引数リストと同じ形式を持っているからだ.
実際,destructuring-bind
はマクロの引数リストを分離するために使われるコードだが,
今ではそれが独立したオペレータになったのだ.
マクロの仮引数リストに入れられるものなら何でもパターンに入れられる.
(ただし些細な例外がある.キーワード&environment
だ.)
束縛をいちどきに生成するのは魅力的な発想だ. 以下の節ではこの考えの変化形を幾つか説明する.
構造化代入をリストに限る理由はなく,複雑なオブジェクトはどれもその候補になる.
この節では,リスト以外のオブジェクト用のdestructuring-bind
類似マクロの書き方を説明する.
(defmacro dbind (pat seq &body body) (let ((gseq (gensym))) `(let ((,gseq ,seq)) ,(dbind-ex (destruc pat gseq #'atom) body)))) (defun destruc (pat seq &optional (atom? #'atom) (n 0)) (if (null pat) nil (let ((rest (cond ((funcall atom? pat) pat) ((eq (car pat) '&rest) (cadr pat)) ((eq (car pat) '&body) (cadr pat)) (t nil)))) (if rest `((,rest (subseq ,seq ,n))) (let ((p (car pat)) (rec (destruc (cdr pat) seq atom? (1+ n)))) (if (funcall atom? p) (cons `(,p (elt ,seq ,n)) rec) (let ((var (gensym))) (cons (cons `(,var (elt ,seq ,n)) (destruc p var atom?)) rec)))))))) (defun dbind-ex (binds body) (if (null binds) `(progn ,@body) `(let ,(mapcar #'(lambda (b) (if (consp (car b)) (car b) b)) binds) ,(dbind-ex (mapcan #'(lambda (b) (if (consp (car b)) (cdr b))) binds) body))))\caption{シークェンス一般に構造化代入を行うオペレータ.} \label{fig:GeneralSeqDestructuringOp}
次の自然な一歩は,シークェンス一般を扱うことだ.
第\ref{fig:GeneralSeqDestructuringOp}図にはdbind
というマクロを示した.
これはdestructuring-bind
と似ているが,任意の種類のシークェンスに対して機能する.
第2引数にはリスト,ヴェクタ,またはそれらの任意の組合せが使える.
> (dbind (a b c) #(1 2 3) (list a b c)) (1 2 3) > (dbind (a (b c) d) '( 1 #(2 3) 4) (list a b c d)) (1 2 3 4) > (dbind (a (b . c) &rest d) '(1 "fribble" 2 3 4) (list a b c d)) (1 #\f "ribble" (2 3 4))
リードマクロ#(
はヴェクタを表現するためのもので,#\
は文字を表現するためのものだ.
"abc"
=#(#\a #\b #\c)
なので,"fribble"
の第1要素は文字#\f
だ.
話を単純にするため,dbind
は&rest
及び&body
キーワードのみに対応する.
これまでに見た大半のマクロと比べて,dbind
は大きい.
このマクロの実装方法は学んでおく価値がある.
いかに動作するかが理解できるだけでなく,
そこからLispプログラミング一般に通じる教訓が得られるからだ.
第3.4節で触れたように,Lispプログラムは意図的にテストしやすいように書くことができる.
ほとんどのコードでは,そのように書きたい欲求と速度を出す必要性とのバランスを取らなければならない.
うまいことに,第7.8節で説明したように,マクロ展開コードにとって速度は重要ではない.
マクロの展開形を生成するコードを書くときには,自分自身の人生を楽にするように書いてよい.
dbind
の展開形は,2個の関数destruc
とdbind-ex
によって生成される.
恐らく全てを単一パスで行う1個の関数にこれらをまとめることもできるだろう.
しかしどうしてそんなことに拘るだろうか?
2個の別々な関数であれば,テストは簡単だ.
どうしてこの利点を,必要もない速度と交換にかけるだろうか?
1個目の関数destruc
は実行時にパターンを探索し,
各々の変数を対応するオブジェクトの位置と関連づける.
> (destruc '(a b c) 'seq #'atom) ((A (ELT SEQ 0)) (B (ELT SEQ 1)) (C (ELT SEQ 2)))
第3引数はオプションの述語で,それはパターン構造とパターンの内容を区別するのに使われる. アクセスを効率的にするため,新しい変数(gensym)が部分シークェンスそれぞれに束縛される.
> (destruc '(a (b . c) &rest d) 'seq) ((A (ELT SEQ 0)) ((#:G2 (ELT SEQ 1)) (B (ELT #:G2 0)) (C (SUBSEQ #:G2 1))) (D (SUBSEQ SEQ 2)))
destruc
の出力はdbind-ex
に送られ,マクロの膨大な展開形が生成される.
それはdestruc
の生成したツリーを入れ子になった幾つかのlet
に変形する.
> (dbind-ex (destruc '(a (b . c) &rest d) 'seq) '(body)) (LET ((A (ELT SEQ 0)) (#:G4 (ELT SEQ 1)) (D (SUBSEQ SEQ 2))) (LET ((B (ELT #:G4 0)) (C (SUBSEQ #:G4 1))) (PROGN BODY)))
dbind
は,destructuring-bind
と同様に,
探しているリスト構造が全て見つかるものと仮定していることに注意しよう.
対応する式のなかった変数は,multiple-value-bind
のようにnil
には束縛されない.
実行時に与えられたシークェンスに予期された要素が幾つか見つからなかったときは,
構造化代入オペレータはエラーを生成する.
> (dbind (a b c) (list 1 2)) >>Error: 2 is not a valid index for the sequence (1 2)
(defmacro with-matrix (pats ar &body body) (let ((gar (gensym))) `(let ((,gar ,ar)) (let ,(let ((row -1)) (mapcan #'(lambda (pat) (incf row) (setq col -1) (mapcar #'(lambda (p) `(,p (aref ,gar ,row ,(incf col)))) pat)) pats)) ,@body)))) (defmacro with-array (pat ar &body body) (let ((gar (gensym))) `(let ((,gar ,ar)) (let ,(mapcar #'(lambda (p) `(,(car p) (aref ,gar ,@(cdr p)))) pat) ,@body))))\caption{配列に対する構造化代入.} \label{fig:DestructuringOnArray}
内部構造を持つオブジェクトは他に何があるだろうか?
一般の配列がある.
これとヴェクタとの違いは次元が1より大きいことだ.
構造化代入マクロを配列に定義するとして,パターンをどのようにひょうげんすればよいだろう?
2次元配列に対しては,やはりリストを使うのが効率的だ.
第\ref{fig:DestructuringOnArray}図にはマクロwith-matrix
を示した.
これは2次元配列に対して構造化代入を行うためのものだ.
> (setq ar (make-array '(3 3))) #<Simple-Array T (3 3) C2D39E> > (for (r 0 2) (for (c 0 2) (setf (aref ar r c) (+ (* r 10) c)))) NIL > (with-matrix ((a b c) (d e f) (g h i)) ar (list a b c d e f g h i)) (0 1 2 10 11 12 20 21 22)
大きい配列や3次元以上の配列に対しては,異なったアプローチが必要だ. 変数を大きい配列の要素それぞれに束縛したいことはまずない. 配列の疎表現を行うパターンを作る方が実用的だろう. 幾つかの要素だけに対応する変数と,要素を選ぶための座標を含む表現だ. 第\ref{fig:DestructuringOnArray}図の2個目のマクロはこの原則に基づいて作られている. 上記の例の配列の対角成分を求めるにはこのようにする.
> (with-array ((a 0 0) (d 1 1) (i 2 2)) ar (values a d i)) 0 11 22
(defmacro with-struct ((name . fields) struct &body body) (let ((gs (gensym))) `(let ((,gs ,struct)) (let ,(mapcar #'(lambda (f) `(,f (,(symb name f) ,gs))) fields) ,@body))))\caption{構造体に対する構造化代入.} \label{fig:DestructuringOnStruc}
このマクロによって,要素が一定の順番で現れなければならないパターンを卒業しつつある.
変数をdefstruct
で定義された構造体のフィールド(訳注: メンバとも)に束縛するための,
類似のマクロが作れる.
そのようなマクロを第\ref{fig:DestructuringOnStruc}図に示した.
パターンの第1要素は構造体名に関連付けられた一種の接頭辞として,残りはフィールド名として扱われる.
アクセス呼び出しを行うため,このマクロはsymb
(lgwページ)を使っている.
> (defstruct visitor name title firm) VISITOR > (setq theo (make-visitor :name "Theodebert" :title 'king :firm 'franks)) #S(VISITOR NAME "Theodebert" TITLE KING FIRM FRANKS) > (with-struct (visitor- name firm title) theo (list name firm title)) ("Theodebert" FRANKS KING)
CLOSはインスタンスに対して構造化代入を行うマクロを提供する.
tree
が3個のスロット(訳注: クラスのインスタンス変数とも)species
,
age
とheight
を持つクラスで,my-tree
がtree
のインスタンスだとしよう.
次の式の中では,
(with-slots (species age height) my-tree ...)
my-tree
のスロットは,あたかも普通の変数であるかのように参照できる.
with-slots
の本体内では,シンボルheight
はスロットheight
を参照する.
そこに保持された値に束縛されたというだけでなくスロットそのものを参照するので,
次のように書くと,
(setq height 72)
my-tree
のスロットheight
には72という値が与えられる.
このマクロはheight
をスロットの参照に展開されるシンボル・マクロ(第7.11節)として
定義することで機能している.
実際,symbol-macrolet
がCommon Lispに追加されたのは
with-slots
等のマクロを実現するためだ.
with-slots
が本当の構造化代入マクロであろうとなかろうと,
実用的にはdestructuring-bind
と同じ役割を持つ.
古典的な構造化代入が値渡しであるのに対し,この種のものは参照渡しだ.
それを何と呼ぶにしても,便利なのは間違いなさそうだ.
同じ原則に基づいて,どんなマクロが他に定義できるだろうか?
(defmacro with-places (pat seq &body body) (let ((gseq (gensym))) `(let ((,gseq ,seq)) ,(wplac-ex (destruc pat gseq #'atom) body)))) (defun wplac-ex (binds body) (if (null binds) `(progn ,@body) `(symbol-macrolet ,(mapcar #'(lambda (b) (if (consp (car b)) (car b) b)) binds) ,(wplac-ex (mapcan #'(lambda (b) (if (consp (car b)) (cdr b))) binds) body))))\caption{シークェンスに対する構造化代入の参照渡し版.} \label{fig:RefDestructuringOnSeq}
任意の構造化代入マクロの参照渡し版が,
let
でなくsymbol-macrolet
に展開するようにすることで定義できる.
第\ref{fig:RefDestructuringOnSeq}図には,
dbind
をwith-slots
のように振る舞うように修正したものを示した.
with-places
は,dbind
を使うのと同じように使える.
> (with-places (a b c) #(1 2 3) (list a b c)) (1 2 3)
しかしこの新マクロを使うと,with-slots
で行うのと同様に,
シークェンス内のある位置にsetf
を適用することもできる.
> (let ((lst '(1 (2 3) 4))) (with-places (a (b . c) d) lst (setf a 'uno) (setf c '(tre))) lst) (UNO (2 TRE) 4)
ここではwith-slots
の場合と同様,変数は構造体の対応する場所を参照している.
しかし他にも重要な違いがある.
それらの疑似変数に値を設定するにはsetq
でなくsetf
を使わなければならない.
マクロwith-slots
は本体内で使われたsetq
をsetf
に置き換えるために
code-walker
(rcxページ)を呼び出さなければならない.
ここでcode-walker
を書いても,コード量の割に改善点は小さくなってしまう.
with-places
がdbind
より一般的だと言うなら,どうしてそちらを常に使わないのだろうか?
dbind
は変数を値に束縛するが,
with-places
は変数を,値を見つけるための一連の指示に関連付ける.
参照の度に検索が必要になる.
dbind
が変数c
を(elt x 2)
の値に束縛する所を,
with-places
はc
を(elt x 2)
に展開されるシンボル・マクロにする.
だからc
が本体内でn回評価されるときは,elt
の呼び出しをn回伴う.
構造化代入で作られた変数に本当にsetf
を使いたいときでなければ,
dbind
の方が速いだろう.
with-places
の定義はdbind
の定義(第\ref{fig:GeneralSeqDestructuringOp}と
わずかに異なるだけだ.
wplac-ex
(dbind-ex
の改良版)の中ではlet
はsymbol-macrolet
に変わっている.
似たような変更によって,任意の普通の構造化代入マクロの参照渡し版を作れる.
構造化代入が代入の一般化であるのと同様,パターンマッチングは構造化代入の一般化だ.
「パターンマッチング」という言葉には幾つも意味がある.
ここでの文脈では,それは(ときには変数を含む)2つの構造を比較し,
それらを同一にするような変数への値の代入方法がないかどうか調べることだ.
例えば?x
と?y
が変数ならば,
次の2つのリストは?x
= a
かつ?y
= b
のときにマッチする.
(p ?x ?y c ?x) (p a b c a)
また次のリストは?x
= ?y
= c
のときにマッチする.
(p ?x b ?y a) (p ?y b c a)
外部とメッセージをやりとりすることで動作するプログラムを考えよう. メッセージに反応するには,そのメッセージがどの種類のものか判別しなければならず, またそのメッセージの内容を抽出しなければならない. マッチング・オペレータを使うと,2つの段階を組み合わせられる.
そのようなオペレータを書けるようにするには,変数を他から区別する方法が必要になる.
シンボルはみな変数だなどと言うことはできない.
シンボルには引数としてパターン内に現れてほしいからだ.
ここではパターン変数はクエスチョンマークで始まる変数としよう.
それで不便なことがあったら,述語var?
を再定義するだけでその決まりを変更できる.
(defun match (x y &optional binds) (acond2 ((or (eql x y) (eql x '_) (eql y '_)) (values binds t)) ((binding x binds) (match it y binds)) ((binding y binds) (match x it binds)) ((varsym? x) (values (cons (cons x y) binds) t)) ((varsym? y) (values (cons (cons y x) binds) t)) ((and (consp x) (consp y) (match (car x) (car y) binds)) (match (cdr x) (cdr y) it)) (t (values nil nil)))) (defun varsym? (x) (and (symbolp x) (eq (char (symbol-name x) 0) #\?))) (defun binding (x binds) (labels ((recbind (x binds) (aif (assoc x binds) (or (recbind (cdr it) binds) it)))) (let ((b (recbind x binds))) (values (cdr b) b))))\caption{マッチング関数.} \label{fig:MatchingFunc}
第\ref{fig:MatchingFunc}図には,幾つかのLisp入門書に出て来るのと似たパターンマッチング関数を示した.
match
には2つのリストを与える.
それらがマッチするようにできるならば,そのマッチを実現するリストが返される.
> (match '(p a b c a) '(p ?x ?y c ?x)) ((?Y . B) (?X . A)) T > (match '(p ?x b ?y a) '(p ?y b c a)) ((?Y . C) (?X . ?Y)) T > (match '(a b c) '(a a a)) NIL NIL
match
は引数を要素毎に比較しつつ,
束縛と呼ばれる,変数への値の関連付けを生成し,変数binds
の中に保持する.
match
はマッチに成功すると生成された束縛を返し,失敗するとnil
を返す.
マッチが成功しても必ずしも束縛が生成される訳ではないので,
match
はgethash
と同様に第2返り値でマッチに成功したかどうかを表す.
> (match '(p ?x) '(p ?x)) NIL T
match
が上のようにnil
とt
を返したときは,
マッチに成功したが束縛は作られなかったことを示す.
(defmacro if-match (pat seq then &optional else) `(aif2 (match ',pat ,seq) (let ,(mapcar #'(lambda (v) `(,v (binding ',v it))) (vars-in then #'atom)) ,then) ,else)) (defun vars-in (expr &optional (atom? #'atom)) (if (funcall atom? expr) (if (var? expr) (list expr)) (union (vars-in (car expr) atom?) (vars-in (cdr expr) atom?)))) (defun var? (x) (and (symbolp x) (eq (char (symbol-name x) 0) #\?)))\caption{遅いマッチングオペレータ} \label{fig:SlowMatchingOp}
Prologと同様,match
は_
(下線)をワイルドカードとして扱う.
これは全てにマッチし,束縛には影響しない.
> (match '(a ?x b) '(_ 1 _)) ((?X . 1)) T
match
があれば,パターンマッチングを行うdbind
を書くのは容易だ.
第\ref{fig:SlowMatchingOp}図にはif-match
というマクロを示した.
dbind
と同様,第1,第2引数はパターンとシークェンスで,
パターンをシークェンスと比較することで束縛が生成される.
しかし本体の代わりに引数がさらに2つある.
match
がマッチングに成功したとき,そこで作られた束縛の下で評価されるthen節と,
マッチングに失敗したときに評価されるelse節だ.
次にif-match
を使った簡単な関数を示す.
(defun abab (seq) (if-match (?x ?y ?x ?y) seq (values ?x ?y) nil))
マッチに成功すると,?x
と?y
に対応する値が定められ,返り値になる.
> (abab '(hi ho hi ho)) HI HO
関数vars-in
はパターン内の全てのパターン変数を返す.
これはvar?
を呼んで変数かどうかの判断を行っている.
この時点ではvar?
はvarsym?
(第\ref{fig:MatchingFunc}図)と同一の関数だ.
これは束縛リスト内の変数を判別するのに使われる.
2つの関数を個別に用意したのは, 2種類の変数に異なった表現を使いたくなったときのためだ.
第\ref{fig:SlowMatchingOp}図を見ると,if-match
は短いが,余り効率がよくない.
処理を実行時に行い過ぎる.
第1のシークェンスがコンパイル時に分かっているときですら,
両方のシークェンスを実行時に探索するようになっている.
さらにいけないのが,
マッチングの過程で変数束縛を保持するためにリストを一々コンシングしている点だ.
コンパイル時に知られている情報を活用してif-match
の新版を書けば,
不必要な比較を行わず,全くコンシングしないようにできる.
シークェンスの片方がコンパイル時に知られており,そちらだけが変数を含むときは,別のやり方で行ける.
match
を呼び出すときは,どちらの引数が変数を含んでもよいことになっている.
変数をif-match
の第1引数のみに制限することで,
コンパイル時にどの変数がマッチングに関わるかを判定できる.
すると変数束縛のリストを作らずに,変数の値を変数そのものの中に保持することができる.
(defmacro if-match (pat seq then &optional else) `(let ,(mapcar #'(lambda (v) `(,v ',(gensym))) (vars-in pat #'simple?)) (pat-match ,pat ,seq ,then ,else))) (defmacro pat-match (pat seq then else) (if (simple? pat) (match1 `((,pat ,seq)) then else) (with-gensyms (gseq gelse) `(labels ((,gelse () ,else)) ,(gen-match (cons (list gseq seq) (destruc pat gseq #'simple?)) then `(,gelse)))))) (defun simple? (x) (or (atom x) (eq (car x) 'quote))) (defun gen-match (refs then else) (if (null refs) then (let ((then (gen-match (cdr refs) then else))) (if (simple? (caar refs)) (match1 refs then else) (gen-match (car refs) then else))))) (defun match1 (refs then else) (dbind ((pat expr) . rest) refs (cond ((gensym? pat) `(let ((,pat ,expr)) (if (and (typep ,pat 'sequence) ,(length-test pat rest)) ,then ,else))) ((eq pat '_) then) ((var? pat) (let ((ge (gensym))) `(let ((,ge ,expr)) (if (or (gensym? ,pat) (equal ,pat ,ge)) (let ((,pat ,ge)) ,then) ,else)))) (t `(if (equal ,pat ,expr) ,then ,else))))) (defun gensym? (s) (and (symbolp s) (not (symbol-package s)))) (defun length-test (pat rest) (let ((fin (caadar (last rest)))) (if (or (consp fin) (eq fin 'elt)) `(= (length ,pat) ,(length rest)) `(> (length ,pat) ,(- (length rest) 2)))))\caption{高速マッチング・オペレータ.} \label{fig:FastMatchOp}
if-match
の新版は第\ref{fig:FastMatchOp}図に示した.
どのコードが実行時に評価されるか予測がつくときは,コンパイル時にコードを生成できる.
ここではmatch
の呼び出しへ展開させる代わりに,必要な比較だけを行うコードを生成することにする.
変数?x
を使って?x
の束縛を保持するつもりなら,
まだマッチングによって束縛が決定されていない変数をどのように表現すればよいだろうか?
ここではパターン変数がgensymに束縛されているときに未束縛を表すことにする.
よってif-match
は,パターン内の変数を全てgensymに束縛するコードの生成から始まる.
この場合with-gensyms
へと展開せずに,コンパイル時に一度にgensymを生成して,
展開形内に直接挿入しても大丈夫だ.
展開形の残りはpat-match
が生成する.
このマクロはif-match
と同じ引数を取る.
唯一の違いは,こちらはパターン変数には新しい束縛を設けない点だ.
状況によってはこの方が都合がよい.
また第19章ではpat-match
ならではの用法が出て来る.
新マッチングオペレータでは,
パターンの中身とパターンの構造との区別は関数simple?
によって定義される.
パターン内にクォートされたリテラルを使えるようにしたいなら,
構造化代入を行うコード(及びvars-in
)は,
第1要素がクォートであるリストの中には進まないように指示されなくてはならない.
新マッチング・オペレータでは,単にリストをクォートするだけでパターンの要素として使える.
dbind
と同様,pat-match
はdestruc
を呼んで,
実行時に引数を切り分けてくれる関数呼び出しのリストを得ている.
このリストは,ネストしたパターン用のマッチングコードを再帰的に生成するgen-match
に渡され,
次に,パターンのツリーの葉それぞれに対するマッチングコードを生成するmatch1
に渡される.
if-match
の展開形内のコードの大部分は,第\ref{fig:FastMatchOp}図のmatch1
に因るものだ.
この関数は4通りの場合を考慮している.
パターンの要素がgensymのときは,
それは部分リストの保持のためにdestruc
に作られた不可視な変数の一つであって,
実行時に行う必要があるのは,それが適切な長さを持っているか調べることだけだ.
要素がワイルドカード(_
)のときは,コードを生成する必要はない.
要素が変数のときは,match1
が,
その変数を実行時に与えられるシークェンスの対応する部分に対してマッチするコード
またはその変数に対応する部分を代入するコードを生成する.
これら以外のときは,要素はリテラル値として扱われ,
match1
はそれをシークェンスの対応する場所と比較するコードを生成する.
(if-match (?x 'a) seq (print ?x))これは次のように展開される:
(let ((?x '#:g1)) (labels ((#:g3 nil nil)) (let ((#:g2 seq)) (if (and (typep #:g2 'sequence) (= (length #:g2) 2)) (let ((#:g5 (elt #:g2 0))) (if (or (gensym? x) (equal ?x #:g5)) (let ((?x #:g5)) (if (equal 'a (elt #:g2 1)) (print ?x) (#:g3))) (#:g3))) (#:g3)))))\caption{
if-match
の展開形.}
\label{fig:ExpansionOfIfmatch}
展開形の一部が生成される様子の例を見よう. 次のマクロ呼び出しから始める.
(if-match (?x 'a) seq (print ?x) nil)
パターンは,シークェンスを表現する幾つかのgensym(読み易くするためにg
と呼ぼう)
と共にdestruc
に渡される.
(destruc '(?x 'a) 'g #'simple?)
この結果は次のようになる.
((?x (elt g 0)) ((quote a) (elt g 1)))
このリストの先頭に(g seq)
をコンスし,
((g seq) (?x (elt g 0)) ((quote a) (elt g 1)))
この全体をgen-match
に送る.
length
のナイーヴな実装(grsページ)と同様に,
gen-match
は最初にリスト末尾までえんえんと再帰的に作用し,戻る過程で返り値を構築する.
要素が尽きるとgen-match
はthen部を返すが,それが?x
になる.
再帰から戻る過程で,この返り値はmatch1
にthen部として渡される.
こうして次のような呼び出しが得られ,
(match1 '(((quote a) (elt g 1))) '(print ?x) ' else function )
この結果は次のようになる.
(if (equal (quote a) (elt g 1)) (print ?x) else function)
今度はこれがmatch1
の別の呼び出しのthen部になり,
その値が最後のmatch1
のthen部になる.
このif-match
の展開形の全貌は第\ref{fig:ExpansionOfIfmatch}図に示した.
この展開形では,gensymは全く関連のない2通りで使われている.
まず実行時にツリーの一部を保持する変数は,捕捉を避けるためにgensymの名前を持っている.
また変数?x
の初期値には,
マッチングによって値を代入されていないことを表すためgensymが使われる.
新しいif-match
では,パターンの要素は,暗黙の内にクォートされずに評価される.
これはLispの変数が,クォートされた式と同様にパターン内に使えるということだ.
> (let ((n 3)) (if-match (?x n 'n '(a b)) '(1 3 n (a b)) ?x)) 1
新版はdestruc
(第\ref{fig:GeneralSeqDestructuringOp}図)を呼んでいるので,
改善点がさらに2つ出て来る.
パターンにキーワード&rest
や&body
が使えるようになった
(match
はこれらがあって問題にはならない).
またdestruc
はシークェンス一般用のオペレータelt
とsubseq
を使っているので,
if-match
の新版はどのようなシークェンスについても使える.
abab
が新版if-match
で定義されると,ヴェクタや文字列にも使えるようになる.
> (abab "abab") #\a #\b > (abab #(1 2 1 2)) 1 2
実際,パターンはdbind
に渡せるものと同じ位複雑であってもよい.
> (if-match (?x (1 . ?y) . ?x) '((a b) #(1 2 3) a b) (values ?x ?y)) (A B) #(2 3)
第2返り値でヴェクタの要素が表示されていることに注意.
ヴェクタをこのように表示するには,*print-array*
をt
に設定すること.
この章では,プログラミングの新たな領域への一歩を踏み出した.
構造化代入のための単純なマクロから話は始まった.
if-match
の最終版では,むしろ独自の言語に見えるようなものが得られた.
以降の章では,同じ考えに基づいて働く種類のプログラム全体について説明する.