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,。這會強制嘗試回溯,但是中斷阻止了回溯,所以查詢會失敗。

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

確實可以這么做。關鍵點在於第一個子句是從本質上描述了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還是其他的任何語言),理解需要解決的問題,找到合適的解決方案。然后:盡你所能地嘗試和完善!


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com