1. 程式人生 > >4.4.2 查詢系統如何工作

4.4.2 查詢系統如何工作

4.4.2 查詢系統如何工作
在4.4.4部分中我們將把查詢直譯器的實現程式表示為一系列的程式的組合而成的集合。在這一部分中,
我們給出一個概述,來解釋系統的通用的結構,它是獨立於低層的實現細節的。在描述瞭解釋器的實現
之後,我們將處於一個位置,也就是理解了查詢語言的邏輯操作與數學的邏輯操作的不同之處,其中的
一些侷限之處,一些很微妙的方式。

為了把查詢與資料庫中的事實與規則進行匹配,查詢的直譯器必須執行一些型別的搜尋,
應該是很明顯的事了。完成這個任務的一種方式是實現查詢系統為一個非確定性的程式,
使用4.3部分中的amb直譯器(見練習4.78)。另一個可行的方法是在流的幫助下,
管理好搜尋。我們的實現採用第二種方法。

組織起查詢系統,是圍繞著兩種中心的操作的,它叫做模式匹配和統一。我們首先描述
模式匹配,然後解釋這種操作如何結合資訊的組織,用流的幀,讓我們能夠實現簡單的
和複合的查詢。我們接下來討論統一,模式匹配的泛化需要實現規則。最後,我們顯示
整個查詢直譯器如何結合一個分類的程式,以一種與eval分類表示式相似的方式,
而這種分類是在4.1部分中描述的直譯器中實現的。

*模式匹配
一個模式匹配器是一個程式,它測試一些資料是否符合一個特定的模式。例如,
資料列表((a b)  c  (a  b)) 匹配模式 (?x   c  ?x)並且模式變數?x 繫結為(a  b).
相同的資料列表匹配模式 (?x  ?y  ?z)並且模式變數 ?x 和?z都繫結到(a  b),?y
繫結到c。它也能匹配模式((?x   ?y)  c  (?x   ?y))並且?x 繫結到 a 和 ?y 繫結到 b。
然而,它不能匹配模式(?x  a  ?y),因為模式要求一個列表的第二個元素是符號a. 

查詢系統使用的模式匹配器,它有三個引數,是一個模式,一個數據,一個幀它指定了
多個模式變數的繫結情況。它以與幀中的繫結一致的方式來檢查資料是否匹配模式。
如果匹配,它返回給定的幀,而幀的繫結是由匹配來確定的。否則它顯示匹配失敗。

例如,使用模式(?x ?y ?x)來匹配(a  b  a),還有一個空的幀,將返回一個幀,它指定了
?x 綁定了 a和  ?y 綁定了b。試著匹配,以相同的模式和相同的資料,還有一個幀,它指定了
?y 綁定了a,將匹配失敗。試著匹配,以相同的模式和相同的資料,還有一個幀,它指定了
?y 綁定了b,而?x沒有繫結,將返回給定的幀,幀中新增了一個繫結,是x 綁定了 a。

為了處理沒有包括規則的簡單的查詢,模式匹配器就是所需要的所有的機制了。
例如,處理如下的查詢

(job  ?x  (computer programmer))

我們能掃描資料庫中的所有的記錄,查詢 並且匹配模式,並且對應一個初始化為空的幀。
對於我們發現的任何一個匹配,我們使用被匹配返回的幀,匹配在例項化模式時以a代替?x.

* 幀的流
帶有幀的模式測試被組織為使用流的方式。給定的一個單獨的幀,
匹配過程的執行是通過對資料庫一條條記錄的檢查。對於任何一條資料庫的記錄,
匹配器生成一個特定的符號來顯示匹配已經失敗或者是對幀進行擴充套件。所有的資料庫的記錄
的結果被收集到一個流中,通過一個過濾器,剔除失敗的。結果是對給定的幀進行擴充套件的所有
的幀的流和對資料庫中的一些記錄的匹配。

在我們的系統中,一個查詢以一個幀的輸入流,並且針對流中的每個幀,執行如上的匹配操作。
正如在圖4.4中,顯示的那樣。也就是說,在輸入流中的每個幀,查詢生成一個新的流,通過匹配
資料庫中的記錄來擴充套件那個幀。所有的這些流然後組合成一個巨大的流,它包括了輸入流中所有的幀的
所有的可能的擴充套件。這個流是查詢的輸出。

幀的輸入流        ————————  幀的輸出流 過濾並擴充套件
——————>|查詢(job  ?x  ?y)    |——————>
                        |————————|
                                     ^
                                      |
                                      |
                                   資料庫中記錄的流

圖4.4   一個查詢處理幀的圖

為了回答一個簡單的查詢,我們使用帶有輸入流的查詢,流由一個單獨的空幀組成。輸出的結果流
包括了對空幀的所有的擴充套件(也就是對於我們的查詢的所有的結果)這個幀的流然後被用來生成原來的
查詢模式的一個複製的流,並且模式的變數被幀中的值例項化了,這是最終被打印出來的流。

* 複合的查詢
當我們處理複合的查詢時,幀的流的實現是真正的優雅是很明顯的。複合的查詢的處理
利用了我們的匹配器的能力,它要求一個匹配與特定的幀保持一致。例如,要處理兩個查詢的and,
例如

(and  (can-do-job  ?x  (computer  programmer  trainee)) 
         (job  ?person ?x))

(正式的,“找到所有的做計算機程式設計師培訓的工作的人”),我們首先找到匹配如下的模式的
所有的記錄
 (can-do-job  ?x  (computer  programmer  trainee)

這產生了幀的流,它的任何一個元素都包括了?x的繫結。然後對於在流中任何一個幀
我們找所有的匹配(job ?person ?x)的記錄。以與給定的?x的繫結一致的方式進行。
任何一次匹配將產生一個包括了?person 和?x的繫結的幀。這兩個查詢的與,能被視
作這兩個單獨的查詢的序列的組合,如圖4.5所示。幀通過第一個查詢的過濾器被過濾,
再進而被第二個查詢擴充套件。

幀的輸入流        ————————  幀的輸出流
                        |   (and  A   B)    |
——————  |—>|  A  |—>| B | - |-————————>
                        |——^——— ^— |
                                      |
                                      |
                                      |
                                   資料庫

圖4.5   通過操作幀的流,生成兩個查詢的與操作組合。

圖4.6顯示了計算兩個查詢的或操作的相似的方法,作為兩個單獨的查詢的併發的組合。
輸入的流被單獨的擴充套件。兩個結果流然後被合併來生成最終的輸出流。

幀的輸入流        ————————  幀的輸出流
                        |   (or  A   B)        |
                        |                               |
                        |  | >|  A  |------\/     |
——————  |-|      ^     |merge|   |———>
                        |  |—>| B | ---—^    |
                        |         |  ^                |
                        |————|——— —|
                                       |
                                       |
                                   資料庫
圖4.6  通過併發的和合並結果,操作幀的流,生成兩個查詢的或操作組合。

甚至從這種高層次的描述,複合的查詢的處理能夠是很低效的,這也是很明顯的事。
例如,因為一個查詢可能為一個輸入流生成多個輸出流,並且任何在與操作中的一個查詢要從
之前的查詢中得到輸入流,一個與查詢,最壞的情況下,不得不執行查詢個數的指數級的匹配個數。
(見練習4.76)儘管系統處理僅簡單的查詢是可行的,但是處理複雜的查詢是相當困難的。

從幀的流的視角上看,一些查詢的非操作的行為像一個過濾器,它移除了能被滿足的查詢。例如,
給出如下的模式

(not  (job  ?x  (computer  programmer)))

我們試著為輸入流的任何一個幀,生成擴充套件的幀,來滿足(job  ?x  (computer  programmer))。
然後我們從輸入流中移除所有的幀。結果是流僅包括了那些幀,幀中的繫結不滿足(job  ?x  (computer  programmer))。 例如,在處理查詢

(and (supervisor ?x  ?y)
  (not  (job  ?x  (computer  programmer))))

第一個子句生成了幀,綁定了?x 和 ?y。非子句過濾了這些內容,移除了所有的滿足?x是
一個程式設計師的條件限制。

lisp-value的識別符號的實現作為在幀的流上的一個相似的過濾器。我們使用流上的每個幀
來例項化模式上的任何變數,然後應用Lisp的判斷式。我們從輸入流上移除
所有的讓判斷式失敗的幀。

* 統一
在查詢語言中,為了處理規則,我們必須能夠找到規則的結論部分能夠匹配
一個給定的查詢模式的規則。規則的結論像一個斷言,除了它們能包括變數,
所以我們需要一個模式匹配的泛化,叫做統一,在這種情況下,模式和資料
可能都包括變數。

一個統一器以兩個模式為引數,任何一個模式都包括常數和變數,確定是否可能
賦值給變數,並且讓兩個模式相等。如果是這樣的,返回一個包括了這些繫結的幀。
例如,統一化 (?x   a   ?y) 和 (?y  ?z   a) 將指定一個幀,它的變數?x,?y,?z都繫結到a.
另一個方面,統一化(?x  ?y  a)  和(?x  b   ?y)將失敗。因為沒有一個?y的值能讓這兩個
模式相等。(為了讓模式的第二個元素相等,?y必須等於b,然而,為了讓第三個元素相等,
?y必須等於a.)在查詢系統中使用統一器,像模式匹配器一樣,以一個幀為輸入,執行
統一操作,與這個幀的內容要保持一致。

統一化的演算法是查詢系統 中最有技術難度的部分。在複雜的模式中,執行統一操作,
可能似乎需要推導。為了統一化(?x  ?x)  和 ((a  ?y   c)   (a   b  ?z)) , 例如演算法必須推理得到
?x 應該是(a b c),?y 應該是b, ?z 應該是 c. 我們可能認為這個過程像是在模式的元件之間
解一系列的方程。總之,這些同時性的方程,可能要求有大量的操作來求解。例如,
統一化(?x  ?x)  和 ((a  ?y   c)   (a   b  ?z)) 可能被認為是如指定同時性的方程

?x  =  (a  ?y   c)
?x  =  (a  b    ?z)

這些方程應用為

 (a  ?y   c)=  (a  b    ?z)

進一步推導為

a=a, ?y=b,c=?z,

並且因此得到

?x= (a b c)

在一個成功的模式匹配,所有的模式變數都被繫結,它們被繫結的值僅包括
常數。我們已經看到過的所有的例子中都是這樣的。總之,然而,一個成功的
統一可能沒有完全地確定了變數的值,一些變數可能保持著未繫結的狀態,其它的
變數可能被繫結的值中含有變數。

考慮(?x   a) 和 ((b  ?y)   ?z)的統一。我們能夠推導得到?x=(b  ?y)和
a=?z,但是我們不能進一步地得到?x,?y的值了。統一沒有失敗,因為
通過給?x,?y賦值, 肯定可能有值讓這兩個模式相等。因為這個匹配沒有限制
?y的值,所以?y的未繫結的狀態進入了結果幀。這個匹配的操作,限制了?x的值
。無論?y的值是什麼,?x的值必須是 (b  ?y). ?x到模式(b  ?y)的繫結因此進入了
結果幀。如果?y的值在稍後被確定了,並且加入到幀中,(通過一個模式匹配或者
是統一,被要求與這個幀保持一致)對?x的繫結將引用這個?y的值。

* 應用規則
統一是查詢系統從規則做引用的元件的核心。為了看看這是如何完成的,
考慮處理一個包括了應用一個規則的查詢,例如

(lives-near  ?x  (Hacker  Alyssa  P))

為了處理這個查詢,我們首先使用如上描述的普通的模式匹配程式來看看
在資料庫中是否有匹配這個模式的記錄。(在這個例子中,沒有這樣的記錄,因為
我們的資料庫中,沒有包括誰離誰近的直接記錄)。下一步是試圖統一查詢模式
與每個規則的結論。我們發現模式統一規則的結論

(rule  (lives-near   ?person1  ?person2)  
         (and  (address   ?person1  (?town .   ?rest1))
                  (address   ?person2  (?town .   ?rest2))
                  (not (same  ?person1 ?person2))))

在一個幀中的結果是指定了 ?person2繫結為(Hacker Alyssa P),?x綁定了?person1.
現在,相對於這個幀,我們解釋了由規則的內容體給出複合的查詢。成功的匹配將擴充套件
這個幀,通過提供一個對?person1的繫結,因此,?x的值,我們能用來例項化最初
的查詢模式。

總之,查詢直譯器當嘗試在一個幀(在幀中指定一些模式變數的繫結)中建立一個
查詢模式時,使用如下的方法,來應用一個規則:

  . 統一查詢與規則的結論,如果成功,得到一個原始的幀的擴充套件。
  . 相對於擴充套件的幀,解釋由規則的內容體形成的查詢
注意的是這與在eval/apply的Lisp直譯器中應用一個程式的方法是多麼的相似啊。
  . 綁一定要程式的引數到它的實際引數,來形成一個幀,以擴充套件原來的程式環境。
  .相對於擴充套件的環境,解釋由程式體形成的表示式。

這兩個直譯器之間的相似性並不讓人驚奇。正如程式定義是在lisp中的抽象的方法,
在查詢語言中,規則定義是抽象的方法。在任何一個例子中,我們解開抽象,都是通過
建立合適的繫結,並且解釋規則或者是它們對應的程式體。

* 簡單查詢
在規則缺席的情況下,在這部分中,我們先看到了如何解釋簡單的查詢。
現在我們要看一看如何應用規則,我們能夠描述在使用規則和斷言的情況下,
如何解釋簡單的查詢。

給出查詢模式和一個幀的流,我們生成了輸入流的幀,兩個流:

   .  通過在資料庫中匹配所有的斷言(使用模式匹配器)得到一個擴充套件的幀的流。
   .  通過應用所有的可能的規則(使用統一器)得到一個擴充套件的幀的流。

合併如上的兩個流生成一個流,它由給出的模式能被滿足的,並且與
原幀一致的所有的記錄組成。這些流(在輸入流的任何一幀)現在組合成一個大的流,
因此它包括了原始的輸入流的幀能被擴充套件的所有的記錄,為了生成對給定的模式的匹配。

* 查詢直譯器與驅動迴圈
儘管在匹配操作的複雜性,系統被組織得很像與任何其它語言一樣的一個直譯器。程式協調
匹配操作的叫做qeval,並且它扮演的角色與lisp的eval的程式很相似。qeval以一個查詢和
一個幀的流為引數。它的輸出是一個幀的流,對應於對查詢模式的成功匹配,並且它擴充套件了
輸入流中的一些幀,如圖4.4中的顯示。像eval,qeval分類了表示式(查詢)的不同的型別,
併為它們每個型別分發了一個適合的程式。對於每個識別符號(and,or,not ,lisp-value)和簡單
的查詢都有一個程式。

驅動迴圈與在這一章中的其它的直譯器的驅動迴圈程式是很相似的,從終端中讀取查詢。
對於每一個查詢,它呼叫qeval,帶有一個查詢,一個流和一個空的幀。這將生成所有的
可能的匹配的流。在結果的流中每一幀,它例項化原始的查詢,使用在幀中找到的變數的值。
這個例項化的查詢的流被打印出來。

驅動也檢查特殊的命令assert!,它顯示了輸入的內容不是一個查詢,而是一個斷言或者是
被加入到資料庫中的規則。例如,

(assert! (job  (Bitdiddle  Ben)  (computer wizard)))
(assert! (rule  (wheel  ?person)
        (and  (supervisor  ?middle-manager  ?person) 
                 (supervisor  ?x   ?middle-manager))))