標籤:
內容提要
規則順序
目標順序
終止
Prolog是第一門比較成功的邏輯程式設計語言。邏輯程式設計語言內在實現是簡單和富有魅力的:程式員的工作簡單地說就是描述問題;程式員應該寫下(使用語言的邏輯)聲明性的規格說明
(即,一個知識庫),去描述有趣的狀態、事實和關係;程式員不應該告訴電腦如何去實現,而他根據問一些問題去擷取資訊,邏輯程式設計語言會給出答案。
然而,以上是理想情況,Prolog本身也確實通過一些重要的特徵,往這個方向在努力。但是Prolog不是,重複一次,不是一門完整的邏輯程式設計語言。如果你只是從聲明性方面去思考
Prolog程式,那麼實際使用上去就會十分困難。正如我們之前章節學習到的,Prolog通過特有的方式得出查詢的結果:它會自上而下地搜尋知識庫,從左至右地匹配每個子句的目標,並且
通過回溯從錯誤選擇中進行恢複。這些程式性的方面對你的查詢實際如何進行有很重要的影響。我們已經看過了一些例子在其聲明性和程式性上不匹配(記得 p :- p嗎?),接下來,我們
會繼續看到,Prolog中很容易定義邏輯上相同的,但是實現上卻十分不同的程式。讓我們思考如下的情況。
請回憶之前我們定義的“後輩”程式,這裡我們稱為descend1.pl:
child(anne, bridget).child(bridget, caroline).child(caroline, donna).child(donna, emily).descend(X, Y) :- child(X, Y).descend(X, Y) :- child(X, Z), descend(Z, Y).
這裡我們做一個調整,並稱新的程式為descend2.pl:
child(anne, bridget).child(bridget, caroline).child(caroline, donna).child(donna, emily).descend(X, Y) :- child(X, Z), descend(Z, Y).descend(X, Y) :- child(X, Y).
這裡的修改只是換了一些兩個規則的順序。所以如果只是從純粹的邏輯定義上去理解,是什麼都沒有改變的。但是這種改變帶給程式性上有什麼不同嗎?是的,但不明顯。
比如,如果你查詢所有的情況,將會看到descend1.pl的第一個回答是:
X = anne
Y = bridget
然而descend2.pl的第一個回答是:
X = anne
Y = emily
但是兩個程式產生的答案是相同的,只是順序不一致。這是具有共性的。簡要地說,改變Prolog程式中規則的順序,不會改變程式的行為。
我們繼續,在descend2.pl的基礎上,再進行一點小的修改,變成descend3.pl:
child(anne, bridget).child(bridget, caroline).child(caroline, donna).child(donna, emily).descend(X, Y) :- descend(Z, Y), child(X, Z).descend(X, Y) :- child(X, Y).
請注意不同之處。這裡我們對一個規則中的目標順序進行了調整,而並非調整規則的順序。現在,如果我們純粹是從邏輯定義方面去理解,並沒有任何不同,這和之前的兩個定義
是一樣的含義。但是這個程式的行為已經徹底改變。比如,如果我們進行查詢:
?- descend(anne, emily).
Prolog會報錯(類似“Out of local stack”)。Prolog進入了死迴圈,為什嗎?為了滿足查詢descend(anne, emily),Prolog會使用第一個規則。這意味著下一個目的是滿足查詢:
descend(W1, emily).
這裡引入了新的變數W1。但是為了滿足這個新目標,Prolog又會使用第一個規則,這意味著下一個目標會是:descend(W2, emily),這裡引入了新的變數W2。當然,就會迴圈引入下
一個新的目標descend(W3, emily),接下去又是descend(W4, emily),等等。即,目標順序的改變導致了程式的崩潰。使用標準的術語,這裡我們有一個經典的關於左遞迴規則的例子,
即一個規則的主幹部分最左端的目標是和規則的頭部一樣的。正如我們的例子所示,這種規則會導致非終止的計算。目標順序,特別是左遞迴,當其不能終止時,就會變成一切罪惡之源。
還有,這裡有一個針對規則順序的提醒。我們之前提及規則順序的改變,只會影響其查詢的結果的順序。但是這個結論在非終止程式中是不適用的。為了說明這點,請參考關於“後輩”
代碼的第四次修改,稱為descend4.pl:
child(anne, bridget).child(bridget, caroline).child(caroline, donna).child(donna, emily).
descend(X, Y) :- child(X, Y).
descend(X, Y) :- descend(Z, Y), child(X, Z).
這個程式只是在descend3.pl的基礎上,調整了規則的順序。現在這個程式和其他之前的程式具有一樣的聲明性含義,但是程式性上有所差別。首先,很明顯的是,和descend1.pl和
descend2.pl有明顯的差別,因為descend4.pl包含了左遞迴的規則,它會在進行一些查詢時無法終止計算。比如,我們如果進行下面的查詢,將無法終止計算:
?- descend(anne, emily).
但是descend4.pl在程式性上和descend3.pl也有所不同。規則順序的不同導致了這種差異性。比如,descend3.pl在進行查詢:
?- descend(anne, bridget).
時不會終止;但是descend4.pl在這個查詢中會有結果。因為它會首先使用非遞迴的規則,並且找到答案,終止計算。所以在非終止的程式中,規則順序的改變會導致找到一些額外的
解決方案。但無論如何,目標順序的改變,而非規則順序的改變,會使得程式性完全不同。為了確保計算能夠終止,我們必須注意規則主幹部分的目標順序。因為調整規則的順序,不會
改變非終止程式的本質——最多可以找到一些額外的解決方案而已。
總結一下,以上四個關於“後輩”程式的變種,描述了同樣的問題,但是具體實現上有所不同。descend1.pl和descend2.pl在實現上的不同相對來說比較小:它們會產生相同的解決方案,
但是順序不同。然而descend3.pl和descend4.pl在程式性上的差異和之前兩個更大,因為它們的規則中目標的順序不同。具體而言,這兩個版本都包含了左遞迴規則,都會導致非終止的計算
行為。descend3.pl和descend4.pl在規則順序上有所不同,意味著在某些情況下,descend4.pl可以終止計算,但是descend3.pl不能。
那麼我們如何構建有用的Prolog程式呢?通常你首先需要通過聲明性思考去確定整體的想法(藍圖),即思考如何精確地描述問題。這是解決問題的優先方式,同時也是邏輯編程的靈魂。
但是一旦你完成了這部分工作,就必須結合Prolog的具體實現檢查你的方案。特別是需要檢查規則中目標的順序,從而確保計算能夠終止。規則絕不要寫出規則的主幹最左邊目標和規則頭部
相同的情況,而是應該將觸發遞迴的目標寫到主乾的最右邊,即讓遞迴目標出現在所有非遞迴目標的後面。這樣做會使得Prolog有最多的機會不通過遞迴就找到答案。
Learn Prolog Now 翻譯 - 第三章 - 遞迴 - 第二節,規則順序,目標順序,終止