Learn Prolog Now!第二章,第一節 合一

2.1 合一

在講KB4的時候,我們曾簡單提及合一(prolog把woman(X) 與 woman(mia)合一,X實例化爲mia)。現在是時候仔細看看合一了,這是prolog的最基本思想之一。

回顧一下三種元素:

  1. 常量:原子(vincent)、數字。
  2. 變量:(X,Z3,List)
  3. 複雜元素:如 functor(term_1,...,term_n) .

我們從KB4的例子入手講解prolog合一兩個元素的本質。雖然你已經有了印象,但還有一些細節要注意:

如果兩個元素要合一,必要滿足下面下面兩個條件之一:

  • 這是兩個相同的元素
  • 兩個元素內的變量實例化後,兩個元素相同

舉一些例子來幫助理解:

  • mia和mia合一,它們是相同原子
  • 42和42合一,相同數字
  • X和X合一,相同變量
  • woman(mia) 和 woman(mia) 合一,相同的複雜元素
  • woman(mia) 和 woman(vincent) ,無法合一。不僅不相同,而且沒有包含可以實例化爲相同元素的變量
  • mia和X,合一。雖然它們不同,但是X可以被實例化爲mia,
  • loves(vincent,X) 和 loves(X,mia) ,無法合一。無法找到一個X的實例,使兩元素相等。(loves(vincent,vincent) and loves(vincent,mia),loves(vincent,mia) and loves(mia,mia) )

那麼,變量是如何被實例化相等的呢?prolog告訴我們。當prolog合一兩個元素時,會嘗試變量所有可能的實例化。這讓我們能構建複雜元素(遞歸結構的元素),使合一成爲了一個強大的編程機制。

有了第一印象,再看看合一的精確定義。它不僅告訴我們prolog會合一哪些元素,也會告訴我們prolog是如何操作變量的。

  1. 如果 term1 和 term2是常量,只有在它們都是相同數字或都是相同原子時,才合一。
  2. 如果 term1 是變量,term2是任意元素,則它們合一,term1實例化爲term2。反之亦然。若term1 term2均爲變量,它們均實例化爲對方,我們稱它們爲共享值
  3. 如果 term1 和 term2都是複雜元素,則它們只有在以下情況都符合時會合一
    1. 它們要有相同的函數名和參數數量
    2. 對應的參數要合一
    3. 變量的實例化要匹配(loves(vincent,X) 和 loves(X,mia)就是不匹配的情況)
  4. 當且僅當它們遵循上面三個定義之一時,兩個元素可以合一。

注意第三條定義的結構,它的三條子句完美的展現了複雜元素的(遞歸)結構。

例子

| ?- =(mia,mia).

yes
| ?- =(mia,vicent).

no

結果顯而易見,但我們通常不會這麼查詢。把等號寫在前面很不自然,我們通常用中綴方式來表示(就是把等號放在兩個參數中間):

| ?-  mia  =  mia.

yes

它們兩個可以合一,顯然是因爲其符合定義的第一條,它們兩個都是原子。

| ?- 2  =  2.

yes
| ?- mia  =  vincent. 

no

上面的結果也很顯然。

| ?- 'mia'=mia.

yes

讓我們看看上面發生了什麼。prolog認爲,'mia'和mia是同一個原子。prolog會把'something'看作和something一樣的原子,這個特性在某些類型的程序中很有用,要記好了。

| ?-'2'=2.

no

這又是爲什麼呢?

prolog認爲'2'是原子,而2是數字,不符合第一條定義。

| ?- mia  =  X. 

X = mia

yes

這個也很好懂。

| ?- X  =  Y. 

yes

如第三條所說,XY表示了同一個對象。有的版本prolog會輸出下面的信息:

| ?- X  =  Y. 

X  =  _5071 
Y  =  _5071 
yes

首先要知道_5071是匿名變量,當兩個變量合一,它們共享變量值。prolog爲此開闢了一個新的匿名變量( _5071),XY都分享了這個變量的值,5071沒有什麼特殊的意義,只是prolog創建的一個普通的隨機變量名,就和 _5075和 _6189一樣。

| ?-(X,Y,X)=(Y,1,2).

no

譯者注:我自己對例子做了一些補充。XY共享值,所以他們的值應該相同,所以返回no。

?-  X  =  mia,  X  =  vincent.

no

如果把這兩個目標(goals)拆開運行,prolog都會返回yes。但是prolog先運行了第一個目標,X被實例化爲mia,所以X無法與vincent合一。一個被實例化的變量已經不能算是變量了,它變成了它所實例化的對象。

讓我們看看涉及複雜元素的例子吧:

| ?- k(s(g),Y)  =  k(X,t(k)).

X = s(g)
Y = t(k)

yes

顯然,它符合第三條定義,並且根據第二條定義, s(g) 和 X也可以合一, 所以兩個複雜元素可以合一。

| ?- k(s(g),  t(k))  =  k(X,t(Y)). 

X = s(g)
Y = k

yes
| ?- loves(X,X)  =  loves(marcellus,mia).

no

以此類推,上面的答案也很容易得出。

觸發檢查

合一是一個廣爲人知的概念,計算機科學的很多分支都有用到。它已經被徹底研究過,許多合一的算法都是衆所周知的了。但prolog並未採用標準的合一算法。它選擇了一條捷徑,你要學會它。

考慮一下下面的查詢:

?-  father(X)  =  X.

能合一嗎?標準合一算法會說:“不”。

爲什麼呢?任意選一個元素與X合一。比如你把X實例化爲 father(father(butch)) ,左邊就會變成 father(father(father(butch))),右邊就會變成father(father(butch))。顯然無法合一。無論X實例化爲什麼,左邊都會比右邊多一層father()。標準的算法會發現這點,暫停,告訴我們no。

上面給出的prolog遞歸合一不會這麼做。因爲它的右邊是X,根據第二條定義認定它們可以合一。隨後,把X實例化成左邊,father(X)。然而這裏面有一個X,而X被實例化爲father(X)了,所以prolog意識到,father(X)事實上是father(father(X)),等等等等。prolog會不斷擴充一個沒有結束的序列。

上面都是理論,事實上會發生什麼呢?在一些老的prolog實現上,我們所說的確實會發生,你會看到:

Not  enough  memory  to  complete  query!

還有一長串字符:

X  =  father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father 
         (father(father(father(father(father(father

prolog在拼命的想返回一個正確的實例化元素,然而實例化的過程時無邊無際的,它停不下來。從抽象的數學角度來看,prolog的做法是明智的。直覺上,只有當X實例化爲一個用father構造的無限長的元素時,左邊多出來的那個father造成的影響纔可以被忽略。但我們只能處理有限項目,無限只是一種有趣的抽象的數學。無論prolog怎麼努力,也無法做到無限。

因此讓prolog耗盡內存會讓人很苦惱。更先進的prolog(比如SWI Prolog 或者 SICStus Prolog)實現找到了一種更優雅的方法來處理。它們也認爲這個合一是存在的,但它們不會真的天真的去不斷實例化。他們會發現有一個潛在的問題,然後停下來,宣佈合一存在,輸出以下有限字符來表示這個無限的元素:

father(father(father(father(...))))))))

那麼上面這種無限的元素可不可以用來計算呢?結果取決於你用的prolog實現,有的可以,有的不行。如下例子:

X  =  father(X),  Y  =  father(Y),  X  =  Y.

有的會導致崩潰(X=Y是要合一兩個有限表示的無限元素),儘管如此,在一些現代系統(比如SWI 和 Sicstus)中這樣的合一也會很有用,你可以在你的程序中運用它們。然而,它可能的用處和它的實質不在本書的範圍內。

簡而言之,對於“father(X)是否與X合一”這個問題有三種回答

  • 標準合一算法:no
  • 老的prolog實現:一直運行,直到內存用盡。
  • 更先進的prolog實現:yes,然後返回一個用有限表示的無限元素

沒有什麼是真正正確的答案,你需要理解箇中差別,瞭解你用的prolog實現是如何處理這個問題的。

在這個部分的最後,希望你能在prolog上自己操作一下上面的例子。這裏,我們想再進一步說明prolog的合一和標準的合一之間的區別。在上面例子中,我們可以看出,當它們面對X和father(X)合一時,是有很大不同的。

  • 標準算法:當合一兩個元素前,會先執行觸發檢查。也就是說,當合一一個變量和一個元素時,它們會先檢查元素裏是否存在這個變量。如果存在,算法就認定合一不可能。只有當不存在時,算法纔會嘗試繼續合一。

    換句話說,標準合一算法是悲觀的。它們會先檢查,確定是安全的以後才繼續嘗試合一。所以,標準合一算法永遠不會陷入無盡的嘗試實例化變量,不會出現無窮元素。

  • prolog算法:樂觀的算法。它相信你不會給它危險的東西。所以它走了一個捷徑:它跳過了觸發檢查,直接進行合一。作爲一個編程語言,這是一個很明智的策略。合一是prolog最基礎的操作,所以它需要運行的越快越好。每次都進行檢查就會拖慢速度。所以,悲觀是安全的,但是,樂觀能更快!畢竟,在實際程序中,你幾乎不會做出X = father(X)這樣的操作。

最後一提,prolog有一個執行標準合一(帶有觸發檢查)的內建謂詞:

unify_with_occurs_check/2.

所以,當我們查詢:

?-  unify_with_occurs_check(father(X),X).

no

用合一來編程

合一是prolog的基礎操作,在Prolog的證明搜索(proof search)中起着很關鍵的作用(我們待會會學)。隨着你深入瞭解,合一會別的越來越有趣和重要。如果你要寫一個有用的程序,用複雜元素可以定義那些有趣的概念,合一才能找到你想要的信息。

下面就是一個例子,兩條事實分別定義的什麼是垂直(vertical)線和水平(horizontal)線:

vertical(line(point(X,Y),point(X,Z))). 
    
horizontal(line(point(X,Y),point(Z,Y))).

這個知識庫看起來很簡單有趣,我們仔細看看。首先,這個複雜元素有三層。最內層的是point函子和兩個參數,point(X,Y)就代表了笛卡爾座標系上的一個點。我們有了兩個不同的點,就可以以此定義一條線。所以,兩個point元素就被作爲line函子的兩個參數。XY分別是點到原點的垂直和水平距離。

垂直和水平是線的兩種屬性。所以vertical和horizontal要一個line作參數。vertical/1的定義:由相同X座標的兩點組成的線是垂直的。兩個point的第一個參數都是X,以此來表示“相同的X座標”。

水平也是同理,兩個point的第二個參數都是Y,以此來表示“相同的Y座標”。

我們可以怎麼用知識庫?看下面這個例子:

?-  vertical(line(point(1,1),point(1,3))). 

yes
?-  vertical(line(point(1,1),point(3,2))). 

no
?- horizontal(line(point(1,1),point(2,Y))). 

Y = 1

yes

看下面這個例子:

?- horizontal(line(point(2,3),P)). 

P = point(_1972,3)

yes

這個查詢的意思是,哪一個點P和點(2,3)組成的線是平行線。答案是,任何y座標是3的線。_1972是一個變量,prolog的意思是任意x座標都行。

合一結構在prolog中很強大,遠比這個例子強大。此外,用大量合一編寫的程序也會是高效的。在第七章討論不同列表時,我們會學到一個漂亮的例子。

這樣的編程風格很有用,面對一些自然的層次結構(比如上面的例子),我們就可以用複雜元素來表示這個結構,通過合一來使用它。這種方法在計算機語言學上就非常重要,因爲語言本身是有自然層次結構的(想一想,一個句子可以由名詞詞組和動詞詞組組成,名詞詞組也可以由定語和名詞組成,等等等等)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章