Learn Prolog Now 翻譯 - 第十章 - 中斷和否認 - 第三節, 使用否認做爲失敗斷定

Prolog一個頗有用的特徵就是可讓使用者歸納地描述事物,對其進行抽象。好比咱們若是想描述Vincent喜歡漢堡,能夠這麼寫:編程

enjoys(vincent, X) :- burger(X).

可是在現實中總會存在例外。也許Vincent不喜歡Big Kahuna漢堡。即,正確的規則是:Vincent喜歡漢堡,除了Big Kahuna漢堡。好了,咱們如何在Prolog中描述呢?安全

做爲第一步,先介紹另外一個Prolog內置的謂詞:fail/0。正如它的名字所示,fail/0是一個當Prolog運行到這個目標時會當即失敗的特殊標識。這可能聽上去沒有什麼用處,可是請記住,若是Prolog失敗了,它會嘗試回溯。因此fail/0能夠看做一個強制回溯的指令。並且在和中斷一塊兒使用時,因爲中斷會阻止回溯,這會讓咱們寫出一些有趣的程序,特別是,它能夠定義通用規則中的一些異常和特殊的狀況。工具

思考下面的代碼:性能

enjoys(vincent, X) :- big_kahuna_burger(X), !, fail.
enjoys(vincent, X) :- burger(X).

burger(X) :- big_mac(X).
burger(X) :- big_kahuna_burger(X).
burger(X) :- whopper(X).

big_mac(a).
big_kahuna_burger(b).
big_mac(c).
whopper(d).

前兩行代碼描述了Vincent的喜愛。後六行代碼描述了漢堡的類型和具體四個漢堡:a, b, c, d。假設前兩行真實地描述了Vincent的喜愛(即,他喜愛全部的漢堡除了Big Kahuna漢堡),那麼他應該喜愛漢堡a,c和d,但沒有b。事實上,這是正確的:學習

?- enjoys(vincent, a).
true

?- enjoys(vincent, b).
false

?- enjoys(vincent, c).
true

?- enjoys(vincent, d).
true

這是如何起做用的?關鍵在於第一行代碼中!和fail/0的組合使用(這個甚至有一個名字:稱爲「中斷-失敗」組合)。當咱們進行查詢enjoys(vincent, b)時,會首先使用第一個規則,而後到達中斷。這會提交咱們已經作出的選擇,並且特別須要說明的是,會阻止使用(回溯)第二個規則。可是隨後就會到達fail/0,。這會強制嘗試回溯,可是中斷阻止了回溯,因此查詢會失敗。code

這頗有趣,可是不夠理想。首先,注意規則的順序是關鍵:若是咱們調換了前兩行的順序,就得不到想要的結果了。相似地,中斷也是關鍵:若是咱們移除它,程序也不會按照相同的方式運行(因此這是一個紅色中斷)。簡而言之,咱們獲得的是兩個互相依賴的子句。從這個例子出發,若是咱們可以從中提取一些有用的部分,而且包裝爲更健壯的通用方式會更好。變量

確實能夠這麼作。關鍵點在於第一個子句是從本質上描述了Vincent不喜歡X若是X是一個Big Kahuna漢堡。即,「中斷-失敗」組合看上去就是某種形式的__否認__。事實上,這就是關鍵的抽象:「中斷-失敗」組合讓咱們定義了某種形式的否認稱爲:使用否認做爲失敗斷定。下面是實現:程序

neg(Goal) :- Goal, !, fail.
neg(Goal).

對於任意Prolog目標,neg(Goal)將會爲真當Goal爲假時。總結

使用新定義的謂詞neg/1,咱們能夠更清晰地描述Vincent的喜愛:查詢

enjoys(vincent, X) :- burger(X), neg(big_kahuna_burger(X)).

即,Vincent喜歡X若是X是一個漢堡並且X不是Big Kahuna漢堡。這很接近咱們原始的描述:Vicent喜歡漢堡,除了Big Kahuna漢堡。

使用否認做爲失敗斷定是一個重要的工具。不只僅在於它提供了有用的表述性(描述異常狀況的能力),更在於它提供了相對安全的方式。經過使用否認進行失敗斷定(而不是低層次的中斷-失敗組合形式),咱們能夠更好地進行失敗斷定,避免使用紅色中斷而致使的一些錯誤。事實上,否認做爲失敗斷定十分的有用,以致於成爲了標準Prolog的內置實現,因此咱們不用再定義它了。在標準的Prolog實現中,操做符+就是否認做爲失敗斷定,因此咱們能夠從新定義Vincent的喜愛:

enjoys(vincent, X) :- burger(X), \+ big_kahuan_burger(X).


可是,有一些使用否認做爲失敗斷定的建議:不要認爲否認做爲失敗斷定就是邏輯否。它並非,思考下面的「漢堡」世界:

burger(X) :- big_mac(X).
burger(X) :- big_kahuna_burger(X).
burger(X) :- whopper(X).

big_mac(a).
big_kahuna_burger(b).
big_mac(c).
whopper(d).

若是咱們進行查詢enjoys(vincent, X),獲得正確的回答:

?- enjoys(vincent, X).
X = a;
X = c;
X = d;
false

可是假設咱們重寫了第一行代碼實現:

enjoys(vincent, X) := \+ big_kahuna_burger(X), burger(X).

注意從聲明性來看,這裏沒有什麼不一樣:畢竟,burger(X)和不是big kahuna burger(X)在邏輯上等同於:不是big kahuna burger(X)和burger(X)。然而,下面是咱們進行相同的查詢獲得的結果:

?- enjoys(vincent, X).
false

發生了什麼?在更新後的知識庫中,Prolog首先會判斷+ big_kahuna_burger(X)是否成立,這意味着必須證實big_kahuna_burger(X)失敗。可是這是可以成功的。由於,知識庫中有包含big_kahuna_burger(b)這個事實。因此查詢 + big_kahuna_burger(X)會失敗,同時致使原始查詢也會失敗。在內核中,兩個程序關鍵的不一樣在於原來的版本(可以正常工做的)中,咱們在將變量X初始化後再使用的+,在新的版本中,咱們在變量初始化前就使用了+,這就是關鍵的不一樣。

總結一下,使用否認做爲失敗斷定並不等於邏輯否認,咱們必須理解其程序維度上的含義。然而,這是一個重要的編程思路:一般狀況下,使用否認做爲失敗斷定會優於直接使用紅色中斷的程序。可是,「一般」並不意味着「老是」。有些特殊的時候,使用紅色中斷會更好一些。

好比,假設咱們須要寫出代碼如何以下邏輯:若是a和b都成立,或者a不成立可是c成立,那麼p成立。在否認做爲失敗斷定的幫助下,咱們可以寫出的代碼以下:

p :- a, b.
p :- \+ a, c.

可是設想若是a是一個很複雜的目標,須要很長時間的計算。上面這樣的程序意味着咱們須要計算a兩次,這一般會致使不能接受的性能問題。若是是那樣,可能使用以下的程序會更好:

p :- a, !, b.
p :- c.

注意這裏是一個紅色中斷:移除它會改變程序的行爲。

關於否認做爲失敗斷定的介紹到此爲止,這裏沒有廣泛適用的原則能夠覆蓋到全部的狀況。編程更像是科學的藝術:這使得它更加有趣。你須要儘量熟悉你學習的語言的一切(不管是Prolog, Java, Perl仍是其餘的任何語言),理解須要解決的問題,找到合適的解決方案。而後:盡你所能地嘗試和完善!

相關文章
相關標籤/搜索