構造化代入

構造化代入とは,代入の一般化だ. オペレータsetqsetfは個々の変数に代入を行う. 構造化代入は代入をアクセスと組み合わせる. 第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
  ...)

これは短いだけでなく,形も整っている. ものを読むときには視覚的手がかりは文字上のものより遥かに速く認識される. 後の形ではxyzの関係が示されているが, 前の形では推論が必要になる.

このように単純な場合すら構造化代入によってきれいにできるのだから, 複雑な場合にどれほど読みやすくなるか想像して欲しい. 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個の関数destrucdbind-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個のスロット(訳注: クラスのインスタンス変数とも)speciesageheightを持つクラスで,my-treetreeのインスタンスだとしよう. 次の式の中では,

(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}図には, dbindwith-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は本体内で使われたsetqsetfに置き換えるために code-walker(rcxページ)を呼び出さなければならない. ここでcode-walkerを書いても,コード量の割に改善点は小さくなってしまう.

with-placesdbindより一般的だと言うなら,どうしてそちらを常に使わないのだろうか? dbindは変数を値に束縛するが, with-placesは変数を,値を見つけるための一連の指示に関連付ける. 参照の度に検索が必要になる. dbindが変数c(elt x 2)の値に束縛する所を, with-placesc(elt x 2)に展開されるシンボル・マクロにする. だからcが本体内でn回評価されるときは,eltの呼び出しをn回伴う. 構造化代入で作られた変数に本当にsetfを使いたいときでなければ, dbindの方が速いだろう.

with-placesの定義はdbindの定義(第\ref{fig:GeneralSeqDestructuringOp}と わずかに異なるだけだ. wplac-exdbind-exの改良版)の中ではletsymbol-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を返す. マッチが成功しても必ずしも束縛が生成される訳ではないので, matchgethashと同様に第2返り値でマッチに成功したかどうかを表す.

> (match '(p ?x) '(p ?x))
NIL
T

matchが上のようにniltを返したときは, マッチに成功したが束縛は作られなかったことを示す.

(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-matchdestrucを呼んで, 実行時に引数を切り分けてくれる関数呼び出しのリストを得ている. このリストは,ネストしたパターン用のマッチングコードを再帰的に生成する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はシークェンス一般用のオペレータeltsubseqを使っているので, 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の最終版では,むしろ独自の言語に見えるようなものが得られた. 以降の章では,同じ考えに基づいて働く種類のプログラム全体について説明する.


←: リードマクロ     ↑: On Lisp     →: クエリ・コンパイラ

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