前回作成した関数 match はパターンとデータを照合するものでした。今度はパターンとパターンを照合するユニフィケーション (unification) [*2] を作ってみましょう。関数名は unify とします。unify はパターン変数とパターン変数を照合する分だけ match よりも処理は複雑になります。パターンマッチングの応用では match で十分な場合もありますが、このあとで作成する Prolog 風の簡易エキスパートシステムではユニフィケーションが必要になります。次の実行例を見てください。
    (unify '(太郎 好き コーヒー) '(太郎 好き ?x) nil)
    => ((?x . コーヒー))
    (unify '(太郎 好き コーヒー) '(太郎 ?y コーヒー) nil)
    => ((?y . 好き))
     Lisp > (unify '(花子 好き 紅茶) '(花子 ?x ?y) nil)
    =>  ((?y . 紅茶) (?x . 好き))
match では第 2 引数にパターンを与えることはできませんが、unify はパターンでもかまいません。unify は match と同様に、成功した場合は束縛リストを返し、失敗した場合は fail を返します。
次は、ユニフィケーションの特徴的な例を示しましょう。
    (unify '(花子 ?x ?y) '(花子 ?a ?b) nil)
    => ((?y . ?b) (?x . ?a))
    (unify '(花子 ?x ?y) '(花子 ?x ?y) nil)
    => ((?y . ?y) (?x . ?x))
ユニフィケーションでは、パターン変数の値がパターン変数になることもあります。最後の例のように、自分自身が値となる場合もあります。このような場合でもユニフィケーションは成功するのです。
それでは unify を作っていきましょう。処理は match とほぼ同じですが、第 2 引数にもパターン変数が含まれるため、その処理を追加することになります。
| List 1 : ユニフィケーション | 
|---|
| 
(defun unify (pattern datum binding)
  (cond ((variablep pattern)
         (unify-variable pattern datum binding))
        ((variablep datum)
         (unify-variable datum pattern binding))
        ((and (atom pattern) (atom datum))
         (unify-atoms pattern datum binding))
        ((and (consp pattern) (consp datum))
         (unify-pieces pattern datum binding))
        (t 'fail)))
 | 
引数 datum がパターン変数かチェックする処理を追加しています。unify-atoms と unify-pieces は match を unify に置き換えただけで、match-atoms と match-pieces と同じです。
| List 2 : アトムとリストのユニフィケーション | 
|---|
| 
; アトムとのユニフィケーション
(defun unify-atoms (pattern datum binding)
  (if (equal pattern datum) binding 'fail))
; リストのユニフィケーション
(defun unify-pieces (pattern datum binding)
  (let ((result (unify (car pattern) (car datum) binding)))
    (if (eq result 'fail)
        'fail
        (unify (cdr pattern) (cdr datum) result))))
 | 
次は、パターン変数とのユニフィケーションを行う unify-variable を作ります。unify-variable は match-variable と処理が異なる箇所があります。ユニフィケーションの場合、パターン変数とパターン変数は一致しますが、パターン変数とそれと同じパターン変数を含むパターンとは一致しないからです。次の例を見てください。
    (unify '(太郎 好き ?x) '(太郎 好き (コーヒー ブラック)) nil)
    => ((?x コーヒー ブラック))
    (unify '(太郎 好き (コーヒー ?x)) '(太郎 好き (コーヒー ブラック)) nil)
    => ((?x . ブラック))
    (unify '(太郎 好き ?x) '(太郎 好き (コーヒー ?x)) nil)
    => fail
コーヒーの種類をリストで表すことにしました。最初の例では、?x は (コーヒー ブラック) となります。2 番目の例では、?x はブラックとなり、コーヒーの種類を求めることができます。
それでは、最後の例はどうなるのでしょうか。?x と (コーヒー ?x) を照合させることになります。この場合、最初の ?x は太郎が好きなものを表しているのに、次の ?x はコーヒーの種類を表すことになり矛盾してしまいます。したがって、?x と (コーヒー ?x) は不一致と判定しなくてはいけないのです。
それでは unify-variable を作りましょう。パターン変数と値を束縛リストに追加するときに、値の中に同じパターン変数がないことを確認します。この処理を関数 insidep で行います。
| List 3 : パターン変数とのユニフィケーション | 
|---|
| 
(defun unify-variable (pattern datum binding)
  (let ((value (assoc pattern binding)))
    (if (and value
             (not (eq pattern (cdr value))))
        (unify (cdr value) datum binding)
        (if (insidep pattern datum binding)
            'fail
            (add-binding pattern datum binding)))))
 | 
実は、match-varibale との違いがもうひとつあります。ユニフィケーションの場合、同じパターン変数同士の照合は成功するので、束縛リストの中には、たとえば (?x . ?x) というドット対が含まれることがあります。このため、束縛リストから変数の値を探し、それを使って単純に unify を再帰呼び出しすると困ることが起こるのです。?x の値は ?x ですから、同じことをずっと繰り返すことになり、再帰呼び出しが停止しないのです。これを回避するために、最初の if でパターン変数とその値が異なることを確認しています。
そして、束縛リストにパターン変数と値を追加する前に関数 insidep を呼び出して、同じパターン変数が値の中で使われていないかチェックします。それでは insidep を作りましょう。
| List 4 : 同じパターン変数が含まれているか | 
|---|
| 
(defun insidep (var datum binding)
  (unless (eq var datum)
    (inside-sub-p var datum binding)))
 | 
insidep は引数 datum (パターン変数の値) に引数 var (パターン変数) が含まれていれば t を返し、そうでなければ nil を返します。実際の処理は関数 inside-sub-p で行います。ユニフィケーションは同じパターン変数の照合であれば成功するので、var と datum が同じパターン変数であれば nil を返します。
| List 5 : insidep 本体 | 
|---|
| 
(defun inside-sub-p (var datum binding)
  (cond ((eq var datum) t)
        ((atom datum) nil)
        ((variablep datum)
         (let ((value (assoc datum binding)))
           (if value
               (inside-sub-p var (cdr value) binding))))
        (t
           (or (inside-sub-p var (car datum) binding)
               (inside-sub-p var (cdr datum) binding)))))
 | 
inside-sub-p はリストを car と cdr で分解しながら、パターン変数 var が含まれているかチェックします。最初に var と datum が等しいか eq でチェックします。結果が真であればデータの中に同じパターン変数を見つけたので t を返します。
次に、datum がアトムであれば、これ以上分解できないので nil を返します。datum がパターン変数の場合は、その値に var が含まれているかチェックします。assoc で束縛リストから datum を探索し、値が見つかれば inside-sub-p を再帰呼び出しします。そうでなければ nil を返します。
それ以外の場合は datum はリストなので、car と cdr でリストを分解して inside-sub-p を再帰呼び出しします。変数 var が見つかったら探索を中断して t を返せばいいので、or を使っていることに注意してください。つまり、CAR 部を調べてた結果が t ならば or は t を返しますし、nil であれば次の CDR 部の探索が行われます。
これでプログラムは完成です。実際にプログラムを動かして、いろいろ試してみてくださいね。
束縛リストによる管理方法は、リスト処理が得意な Lisp でよく使われる方法ですが、ここでもうひとつ別の方法を紹介しましょう。それはパターン変数を表すシンボルに値を格納する方法です。Lisp のシンボルは値を格納できますが、その機能はレキシカル変数とスペシャル変数に分かれます。今回の方法では、シンボルをスペシャル変数として使用します。
Common Lisp にはスペシャル変数の値を操作する関数が用意されています。次の図を見てください。
| 図 1 : スペシャル変数の操作関数 | 
|---|
| 
                 ┌──シンボル──┐   set
    boundp ←──│ スペシャル変数 │←─── data 
 (束縛チェック)  └────────┘         ↑
                       ↑   │  symbol-value  │
                       │   └────────┘
                       │
                   makunbound (未束縛にする)
 | 
スペシャル変数には値がセットされていない場合があります。このとき、変数にアクセスするとエラーが発生します。シンボルがスペシャル変数の値を持っているか、つまり束縛されているかチェックする関数が boundp です。スペシャル変数の値を持たないようにする、つまり未束縛状態にする関数が makunbound です。値をセットする関数が set で、値を取り出す関数が symbol-value です。それでは、詳しく説明しましょう。
    boundp symbol
    makunbound symbol
boundp は symbol のスペシャル変数が値を持っていれば t を返し、そうでなければ nil を返します。makunbound は symbol のスペシャル変数を未束縛にします。スペシャル変数は値を持たなくなるので、そのシンボルを評価するとエラーになります。makunbound は未束縛にしたシンボルを返します。簡単な使用例を示しましょう。
    (boundp 'a) => nil
    (setq a 10) => 10
    (boundp 'a) => t
    (makunbound 'a) => a
    (boundp 'a)     => nil
シンボル a には値がセットされていません。ここで setq で a に 10 をセットします。a はレキシカル変数ではないので、スペシャル変数に値がセットされます。したがって boundp で a を調べると t を返します。makubound で a を未束縛状態にすると boundp は nil を返します。
    set symbol value
    symbol-value symbol
set は引数 symbol のスペシャル変数の値を value に変更します。set は関数なので symbol は評価されることに注意してください。評価結果がシンボルでなければエラーとなります。set はレキシカル変数の値を変更できません。set は value を返します。
symbol-value は symbol のスペシャル変数の値を取り出します。set と同じく、レキシカル変数にはアクセスできません。簡単な使用例を示しましょう。
    (set 'a 10) => 10
    (let ((a 100))
          (print (symbol-value 'a))
          (set 'a 20)
          (print a))
    10
    100
    a => 20
まず set でシンボル a のスペシャル変数に 10 をセットします。次に、let でレキシカル変数 a を 100 に設定します。symbol-value で a の値を取り出すと、スペシャル変数の値 10 となります。次に、set で a の値を 20 に変更します。この場合、スペシャル変数の値を変更するので、a の値を print しても 100 のままです。let の実行後、a を表示すると 20 に変更されています。
ここで注意点がひとつあります。今まで説明した set や symbol-value の動作は、変数がレキシカルスコープで管理されていることが前提です。たとえば、defvar で宣言された変数はダイナミックスコープで管理されますが、この場合 set や symbol-value は今までの説明とは異なる動作になります。スコープの違いに十分注意してください。
パターンマッチングの変数管理にスペシャル変数を使う場合、束縛・未束縛の状態をそのままパターン変数に当てはめることができるので便利なのです。ただし、スペシャル変数の値を変更するとその影響はずっと残るので、パターンマッチング終了後は未束縛状態に戻しておく必要があります。したがって、連想リストの場合と同じように、ユニフィケーションが成功したときは束縛した変数のリストを返し、失敗したときは fail を返すことにします。
それでは、スペシャル変数を使った管理方法でユニフィケーションを実現してみましょう。まず、パターン変数を束縛する add-binding から修正します。
| List 6 : 変数に値をセットする | 
|---|
| (defun add-binding (var datum binding) (set var datum) (cons var binding)) | 
値のセットは set を使えば簡単です。var にセットされているシンボルのスペシャル変数に datum の値がセットされます。最後に変数 var を binding に追加します。
次は、束縛された変数をクリアする関数 clear-binding を作ります。
| List 7 : 変数をクリアして 'fail を返す | 
|---|
| 
(defun clear-binding (binding)
  (if (consp binding)
    (map nil #'makunbound binding))
  'fail)
 | 
binding がリストであれば、そこに格納されている変数を makunbound で未束縛にします。列関数 map を使って、各要素に makunbound を適用しています。最後に fail を返します。
それでは、ユニフィケーションを修正しましょう。不一致と判定する処理で、今まで束縛されたパターン変数を未束縛にするため、clear-binding を呼び出すようにします。最初は unify です。
| List 8 : ユニフィケーション | 
|---|
| 
(defun unify (pattern datum binding)
  (cond ((variablep pattern)
         (unify-variable pattern datum binding))
        ((variablep datum)
         (unify-variable datum pattern binding))
        ((and (atom pattern) (atom datum))
         (unify-atoms pattern datum binding))
        ((and (consp pattern) (consp datum))
         (unify-pieces pattern datum binding))
        (t (clear-binding binding))))
 | 
最後の節で fail を返しますが、ここで clear-binding を呼び出して変数束縛をクリアします。
| List 9 : アトムとのユニフィケーション | 
|---|
| 
(defun unify-atoms (pattern datum binding)
  (if (equal pattern datum)
      binding
      (clear-binding binding)))
 | 
unify-atoms は pattern と datum が等しくない場合、clear-binding を呼び出し、変数を未束縛にしてから fail を返します。
リストのユニフィケーション unify-pieces は修正の必要はありません。次は unify-variable を変更します。
| List 10 : パターン変数とのユニフィケーション | 
|---|
| 
(defun unify-variable (var datum binding)
  (if (and (boundp var)
           (not (eq (symbol-value var) var)))
      (unify (symbol-value var) datum binding)
      (if (insidep var datum binding)
          (clear-binding binding)
          (add-binding var datum binding))))
 | 
unify-varibale はパターン変数の値を求める処理を修正します。まず boundp でスペシャル変数に値がセットされていることを確認し、次にその値が自分自身でないことを確認します。変数が束縛されていれば、値を取り出して再度 unify を呼び出します。未束縛の場合は、変数 var が datum 内で使われていないことを確認します。使われていれば clear-binding で束縛をクリアして failを返します。
insidep はそのままでいいのですが、inside-sub-p は修正が必要です。
| List 11 : insidep 本体 | 
|---|
| 
(defun inside-sub-p (var datum binding)
  (cond ((eq var datum) t)
        ((atom datum) nil)
        ((variablep datum)
         (if (and (boundp datum)
                  (not (eq (symbol-value datum) datum)))
             (inside-sub-p var (symbol-value datum) binding)))
        (t
         (or (inside-sub-p var (car datum) binding)
             (inside-sub-p var (cdr datum) binding)))))
 | 
datum がパターン変数の場合、値を取り出す処理を symbol-value に変更します。
これで修正は終わりましたが、このままだと unify は変数リストを返すだけで、その値を見ることができません。そこで、パターン変数から値を求める関数を作ります。
| List 12 : パターン変数の値を求める | 
|---|
| 
(defun variable-value (var)
  (let (value)
    (loop
      (unless (boundp var) (return var))
      (setq value (symbol-value var))
      (cond
        ((eq var value)
         (return value))
        ((variablep value)
         (setq var value))
        ((consp value)
         (return (replace-variable value)))
        (t (return value))))))
 | 
variable-value は変数のリンケージをたどって値を求めます。この処理は再帰定義を使わなくても、繰り返しで実現できます。まず boundp で変数 var が束縛されているかチェックします。未束縛であれば、その変数をそのまま返します。ユニフィケーションはパターン変数同士の照合が可能なので、ある変数が未束縛のままということもあるのです。
次に symbol-value で var の値を取り出して、その値をチェックします。もし、var と value が同じシンボルであれば、自分自身が値として格納されています。この場合は、そのシンボルを return で返します。次に value がパターン変数の場合は、value を var にセットして繰り返しを続行します。これで、変数間のリンケージをひとつたどったことになります。
value がリストの場合は、その中にある変数を置換してその結果を return で返します。この処理を関数 replace-variable で行います。最後の節では value をそのまま return で返すだけです。
次は、リスト内のパターン変数をその値で置換する関数 replace-variable を作ります。
| List 13 : パターン変数を置換する | 
|---|
| 
(defun replace-variable (pattern)
  (cond
    ((variablep pattern)
     (variable-value pattern))
    ((atom pattern) pattern)
    (t
     (cons (replace-variable (car pattern))
           (replace-variable (cdr pattern))))))
 | 
replace-variable は再帰を使えば簡単に作れます。pattern がリストの場合は car と cdr で分解し、repalce-varibale を再帰呼び出しします。そして、その返り値を cons で組み立てればよいわけです。
再帰呼び出しの停止条件は pattern が atom の場合ですが、その前に pattern がパターン変数かチェックします。その場合は variable-value で値を求め、その結果を返すようにします。これで、変数をその値に置き換えることができます。
最後に、ユニフィケーションを実行して変数の値を表示する関数 exec-unify を作ります。
| List 14 : ユニフィケーションの実行 | 
|---|
| 
(defun exec-unify(pattern datum)
  (let ((result (unify pattern datum nil)))
    (when (consp result)
      (dolist (var result)
        (format t "~S = ~S~%" var (variable-value var)))
      (clear-binding result))
    result))
 | 
unify で引数 pattern と datum を照合し、その結果を変数 result にセットします。result がリストならばパターン変数に値がセットされています。result に格納された変数を dolist でひとつずつ取り出し、値を variable-value で求めて format で表示します。そして、clear-binding でパターン変数を未束縛の状態に戻してから result を返します。
これでプログラムは完成です。簡単な実行例を示しましょう。
    (exec-unify '(a b c) '(a b c))
    => nil
    (exec-unify '(a b c) '(a b d))
    => fail
    (exec-unify '(a b c) '(?x ?y ?z))
    ?z = c
    ?y = b
    ?x = a
    => (?z ?y ?x)
皆さんも実際にプログラムを実行して、動作を確認してみてくださいね。
;
; unify.l : ユニフィケーション
;
;           Copyright (C) 2003 Makoto Hiroi
;
;
; 要素はパターン変数か
; 
(defun variablep (pattern)
  (and (symbolp pattern)
       (char= #\? (char (string pattern) 0))))
;
; ユニフィケーション
;
(defun unify (pattern datum binding)
  (cond ((variablep pattern)
         (unify-variable pattern datum binding))
        ((variablep datum)
         (unify-variable datum pattern binding))
        ((and (atom pattern) (atom datum))
         (unify-atoms pattern datum binding))
        ((and (consp pattern) (consp datum))
         (unify-pieces pattern datum binding))
        (t 'fail)))
;
; アトムとのユニフィケーション
;
(defun unify-atoms (pattern datum binding)
  (if (equal pattern datum) binding 'fail))
;
; リストのユニフィケーション
;
(defun unify-pieces (pattern datum binding)
  (let ((result (unify (car pattern) (car datum) binding)))
    (if (eq result 'fail)
        'fail
        (unify (cdr pattern) (cdr datum) result))))
;
; 変数とのユニフィケーション
;
(defun unify-variable (pattern datum binding)
  (let ((value (assoc pattern binding)))
    (if (and value
             (not (eq pattern (cdr value))))
        (unify (cdr value) datum binding)
        (if (insidep pattern datum binding)
            'fail
            (add-binding pattern datum binding)))))
;
; datum の中に var があるか
;
(defun insidep (var datum binding)
  (unless (eq var datum)
    (inside-sub-p var datum binding)))
;
; insidep 本体
;
(defun inside-sub-p (var datum binding)
  (cond ((eq var datum) t)
        ((atom datum) nil)
        ((variablep datum)
         (let ((value (assoc datum binding)))
           (if value
               (inside-sub-p var (cdr value) binding))))
        (t
         (or (inside-sub-p var (car datum) binding)
             (inside-sub-p var (cdr datum) binding)))))
;
; unify.l : スペシャル変数を使ったユニフィケーション
;
;           Copyright (C) 2003 Makoto Hiroi
;
;
; 要素はパターン変数か
;
(defun variablep (pattern)
  (and (symbolp pattern)
       (char= #\? (char (string pattern) 0))))
;
; ユニフィケーション
;
(defun unify (pattern datum binding)
  (cond ((variablep pattern)
         (unify-variable pattern datum binding))
        ((variablep datum)
         (unify-variable datum pattern binding))
        ((and (atom pattern) (atom datum))
         (unify-atoms pattern datum binding))
        ((and (consp pattern) (consp datum))
         (unify-pieces pattern datum binding))
        (t (clear-binding binding))))
;
; アトムとのユニフィケーション
;
(defun unify-atoms (pattern datum binding)
  (if (equal pattern datum)
      binding
      (clear-binding binding)))
;
; リストのユニフィケーション
;
(defun unify-pieces (pattern datum binding)
  (let ((result (unify (car pattern) (car datum) binding)))
    (if (eq result 'fail)
        'fail
        (unify (cdr pattern) (cdr datum) result))))
;
; 変数とのユニフィケーション
;
(defun unify-variable (var datum binding)
  (if (and (boundp var)
           (not (eq (symbol-value var) var)))
      (unify (symbol-value var) datum binding)
      (if (insidep var datum binding)
          (clear-binding binding)
          (add-binding var datum binding))))
;
; datum の中に var(変数)があるか
;
(defun insidep (var datum binding)
  (unless (eq var datum)
    (inside-sub-p var datum binding)))
;
; insidep 本体
;
(defun inside-sub-p (var datum binding)
  (cond ((eq var datum) t)
        ((atom datum) nil)
        ((variablep datum)
         (if (and (boundp datum)
                  (not (eq (symbol-value datum) datum)))
             (inside-sub-p var (symbol-value datum) binding)))
        (t
         (or (inside-sub-p var (car datum) binding)
             (inside-sub-p var (cdr datum) binding)))))
;
; 変数を置換する
;
(defun replace-variable (pattern)
  (cond
    ((variablep pattern)
     (variable-value pattern))
    ((atom pattern) pattern)
    (t
     (cons (replace-variable (car pattern))
           (replace-variable (cdr pattern))))))
;
; 変数値を求める
;
(defun variable-value (var)
  (let (value)
    (loop
      (unless (boundp var) (return var)) ; 未束縛
      (setq value (symbol-value var))    ; スペシャル変数を取り出す
      (cond
        ((eq var value)
         (return value))                 ; 自分自身が入っている
        ((variablep value)
         (setq var value))
        ((consp value)                   ; リストの中に変数があるかもしれないので置換する
         (return (replace-variable value)))
        (t (return value))))))
;
; 変数値をセットする
;
(defun add-binding (var datum binding)
  (set var datum)
  (cons var binding))
;
; 変数をクリアして 'fail を返す
;
(defun clear-binding (binding)
  (if (consp binding)
    (map nil #'makunbound binding))
  'fail)
;
; ユニフィケーションの実行
;
(defun exec-unify (pattern datum)
  (let ((result (unify pattern datum nil)))
    (when (consp result)
      (dolist (var result)
        (format t "~S = ~S~%" var (variable-value var)))
      (clear-binding result))
    result))