2.1 合一
在講KB4的時候,我們曾簡單提及合一(prolog把woman(X) 與 woman(mia)合一,X實例化爲mia)。現在是時候仔細看看合一了,這是prolog的最基本思想之一。
回顧一下三種元素:
- 常量:原子(vincent)、數字。
- 變量:(X,Z3,List)
- 複雜元素:如 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是如何操作變量的。
- 如果 term1 和 term2是常量,只有在它們都是相同數字或都是相同原子時,才合一。
- 如果 term1 是變量,term2是任意元素,則它們合一,term1實例化爲term2。反之亦然。若term1 term2均爲變量,它們均實例化爲對方,我們稱它們爲共享值。
- 如果 term1 和 term2都是複雜元素,則它們只有在以下情況都符合時會合一:
- 它們要有相同的函數名和參數數量
- 對應的參數要合一
- 變量的實例化要匹配(loves(vincent,X) 和 loves(X,mia)就是不匹配的情況)
- 當且僅當它們遵循上面三個定義之一時,兩個元素可以合一。
注意第三條定義的結構,它的三條子句完美的展現了複雜元素的(遞歸)結構。
例子
| ?- =(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中很強大,遠比這個例子強大。此外,用大量合一編寫的程序也會是高效的。在第七章討論不同列表時,我們會學到一個漂亮的例子。
這樣的編程風格很有用,面對一些自然的層次結構(比如上面的例子),我們就可以用複雜元素來表示這個結構,通過合一來使用它。這種方法在計算機語言學上就非常重要,因爲語言本身是有自然層次結構的(想一想,一個句子可以由名詞詞組和動詞詞組組成,名詞詞組也可以由定語和名詞組成,等等等等)。