1. 程式人生 > >Prolog教程 14--cut的功能

Prolog教程 14--cut的功能

cut,使用符號!來表示。
直到目前為止,我們都一直在使用Prolog內建的回溯功能。使用此功能可以方便地寫出結構緊湊的謂詞來。

但是,並不是所有的回溯都是必須的,這時我們需要能夠人工地控制回溯過程。Prolog提供了完成此功能的謂詞,他叫做cut,使用符號!來表示。

Cut能夠有效地剔除一些多餘的搜尋。如果在cut處產生回溯,它會自動地失敗,而不去進行其它的選擇。

下面我們將看看它的一些實際的功效。

請參照上圖來理解cut的功能。當在回溯遇到cut時,它改變了回溯的流程,它直接把控制權傳給了上一級目標,而不是它左邊的目標。這樣第一層的中間的那個目標以及第二層!左邊的子目標都不會被Prolog重新滿足。

下面我們將舉個簡單的例子來說明cut的作用。首先加入幾條事實:

data(one).
data(two).
data(three).

下面是沒有使用cut的情況:

cut_test_a(X) :- data(X).
cut_test_a(‘last clause’).

下面是對上面的事實與規則的一次詢問。

?- cut_test_a(X), write(X), nl, fail.
one
two
three
last clause
no

我們再來看看使用了cut之後的情況。

cut_test_b(X) :- data(X), !.
cut_test_b(‘last clause’).

?- cut_test_b(X), write(X), nl, fail.
one
no

我們可以看到,由於在cut_test_b(X)子句加入了cut,data/1子目標與cut_test_b父目標都沒有產生回溯。

下面我們看看把cut放到兩個子目標中的情況。

cut_test_c(X,Y) :- data(X), !, data(Y).
cut_test_c(‘last clause’).

?- cut_test_c(X,Y), write(X-Y), nl, fail.
one - one
one - two
one - three
no

cut抑制了其左邊的子目標data(X)與cut_test_c父目標的回溯,而它右邊的目標則不受影響。

cut是不符合純邏輯學的,不過出於實用的考慮,它還是必須的。過多地使用cut將降低程式的易讀性和易維護性。它就像是其它語言中的goto語句。

當你能夠確信在謂詞中的某一點只有一個答案,或者沒有答案時,使用cut可以提高程式的效率,另外,如果在某種情況下你想讓某個謂詞強制失敗,而不讓它去尋找更多的答案時,使用cut也是個不錯的選擇。

下面將介紹使用cut的技巧。

使用Cut

為了讓冒險遊戲更加有趣,我們來編寫一個小小的迷題。我們把這個迷題叫做puzzle/1。puzzle的引數是遊戲中的某個命令,puzzle將判斷這個命令有沒有特殊的要求,並做出反應。

我們將在puzzle/1中見到cut的兩種用法。我們想要完成的任務是:

如果存在puzzle,並且約束條件成立,就成功。
如果存在puzzle,而約束條件不成立,就失敗。
如果沒有puzzle,成功。
在本遊戲中的puzzle是要到地下室(cellar)中去,而玩家必須擁有手電筒,並且打開了,才能夠進到地下室中。如果這些條件都滿足了,就不需要Prolog再去進行其它的搜尋。所以這裡我們可以使用cut。

puzzle(goto(cellar)):-
have(flashlight),
turned_on(flashlight),
!.

如果約束條件不滿足,Prolog就會通知玩家不能執行命令的原因。在這種情況下,我們也想puzzle謂詞失敗,而不去匹配其它的puzzle子句。因此,此處我們也使用cut來阻止回溯,並且在cut的後面加上fail。

最後一個子句包括了所有非特殊的命令。這裡我們看到,使用cut就像其它語言中的if語句一樣,可以用它來判斷不同的情況。

puzzle(_).

從純邏輯的角度來看,能找到不使用cut而完成同樣功能的方法。這時需要使用內部謂詞not/1。有人認為使用not/1可以使程式更加清晰,不過濫用not同樣也會引起混亂的。

當使用cut時,子句的順序顯得尤為重要了。上例中,puzzle/1的第二個子句可以直接打出錯誤資訊,這是因為我們知道只有當第一個子句在遇到cut前失敗時,Prolog才會考慮第二個子句。

而第三個子句考慮的是最一般的情況,這是因為,前面兩個子句已經考慮了特殊的情況。

如果把所有的cut都去掉,我們就必須改寫第二、三個子句。

puzzle(goto(cellar)):-
not(have(flashlight)),
not(turned_on(flashlight)),
write(‘Scared of dark message’),
fail.

puzzle(X):-
not(X = goto(cellar)).

在這種情況下,子句的順序就無關緊要了。有趣的是,事實上not/1子句可以使用cut來定義,它同時還用到了另一個內部謂詞call/1。call/1把它的引數作為謂詞來呼叫。

not(X) :- call(X), !, fail.
not(X).

在下一章中我們將學習如何在遊戲中加入命令迴圈。那時我們就可以在每次執行玩家的命令之前使用puzzle/1來檢驗它。這裡我們先試試puzzle的功能。

goto(Place) :-
puzzle(goto(Place)),
can_go(Place),
move(Place),
look.

如果玩家現在在廚房裡,並且想到地下室中去。

?- goto(cellar).
It’s dark and you are afraid of the dark.
no

?- goto(office).
You are in the office…

而如果玩家拿著開啟的手電筒,它就可以去地下室了。

?- goto(cellar).
You are in the cellar…