1. 程式人生 > >Prolog教程 15--程式流程控制

Prolog教程 15--程式流程控制

-repeat/0。它在第一次呼叫時永遠成功,並且在回溯時也永遠成功。換句話說,流程不可能回溯通過repeat/0。
如果某個子句中有repeat/0,並且其後有fail/0謂詞出現,那麼將永遠迴圈下去。使用這種方法可以編寫死迴圈的Prolog程式。

在這一章,繼續探索Prolog的程式流程控制,我們將介紹和一般的程式設計語言相似的流程控制。

前面我們使用謂詞fail和write/1來列印出遊戲中所有的物品。這種流程控制類似於一般語言中“do,while”語句。

現在介紹另外一個使用失敗來完成相同功能的內部謂詞—repeat/0。它在第一次呼叫時永遠成功,並且在回溯時也永遠成功。換句話說,流程不可能回溯通過repeat/0。

如果某個子句中有repeat/0,並且其後有fail/0謂詞出現,那麼將永遠迴圈下去。使用這種方法可以編寫死迴圈的Prolog程式。

如果在repeat/0謂詞後面加入幾個中間目標,並且最後使用一個測試條件結束,那麼程式將一直迴圈到條件滿足為止。這相當於其它程式語言中的“do until”。在編寫“尋找Nani”這個遊戲時,我們正好需要這種流程來編寫最高層的命令迴圈。

我們先來看一個例子,它只是簡單的讀入命令並且在螢幕上回顯出來,直到使用者輸入了end命令。內部謂詞read/1可以從控制檯對入一條Prolog字串。此字串必須使用“.”結束,就像所有的Prolog子句一樣。

command_loop:-
repeat,
write('Enter command (end to exit): '),
read(X),
write(X), nl,
X = end.

最後面的那個目標x=end只有在使用者輸入end時才會成功,而repeat/0在回溯時將永遠成功,所以這種結構將使得中將的目標能夠被重複執行。

下面我們要做的事就是加入中間的命令執行部分,而不是簡單的回顯使用者輸入的命令。

我們先來編寫一個新的謂詞do/1,它用來執行我們需要的謂詞。在許多程式語言中,這種結構叫做“do case”,而在Prolog中我們使用多條子句來完成相同的功能。

下面是do/1的程式,我們可以使用do/1來定義命令的同義詞,例如玩家可以輸入goto(X)或者go(X),這都將執行goto(X)子句。

do(goto(X)):-goto(X),!.
do(go(X)):-goto(X),!.
do(inventory):-inventory,!.
do(look):-look,!.

此處的cut有兩個用途。第一,如果我們找到了一條該執行的do子句,就沒有必要去尋找更多的do子句了;其二,它有效地防止了在回溯時又重新執行read目標。

下面是另外的幾條do/1的子句。如果沒有do(end)子句,那麼條件X=end就永遠不會成立,所以end是結束遊戲的命令。最後一個do/1子句考慮不合法的命令。

do(take(X)) :- take(X), !.
do(end).
do(_) :-
write(‘Invalid command’).

下面我們開始正式編寫command_loop/0謂詞,這裡使用前面說編寫的puzzle/1和本章介紹的do/1謂詞來完成命令的解釋工作。並且我們將考慮遊戲結束的情況,遊戲有兩種結束方式,可以是玩家輸入了end命令,或者玩家找到了Nani。我們將編寫一個新的謂詞end_condition/1來完成這個任務。

command_loop:-
write(‘Welcome to Nani Search’), nl,
repeat,
write(’>nani> '),
read(X),
puzzle(X),
do(X), nl,
end_condition(X).

end_condition(end).
end_condition(_) :-
have(nani),
write(‘Congratulations’).

遞迴迴圈

在Prolog程式中使用assert和retract謂詞動態地改變資料庫的方法,不是純邏輯程式的設計方法。就像其他語言中的全域性變數一樣,使用這種謂詞會產生一些不可預測的問題。由於使用了這種謂詞,可是會導致程式中兩個本來應該獨立的部分互相影響。

例如,puzzle(goto(cellar))的結果依賴於turned_on(flashlight)是否存在於資料庫中,而turned_on(flashlight)是使用turn_on謂詞動態地加入到資料庫中的。所以如果turn_on/1中間有錯誤,它就會直接影響到puzzle,這中程式之間的隱形聯絡正是造成錯誤的罪魁禍首。

我們可以重新改造程式,只使用引數傳遞資訊,而不是全域性資料。可以把這種情況想象成一系列的狀態轉換。

在本遊戲中,遊戲的狀態是使用location/2、here/1、have/1以及turned_on/1(turned_off/1)來定義的。我們首先使用這些謂詞定義遊戲的初始狀態,其後玩家的操作將使用assert和retract動態地改變這些狀態,直到最後達到了have(nani)。

我們可以通過定義一個複雜的結構來儲存遊戲的狀態來完成相同的功能,遊戲的命令將把這個結構當作引數進行操作,而不是動態資料庫。

由於邏輯變數是不能通過賦值來改變它們的值的,所以所有的命令都必須有兩個引數,一個是舊的狀態,另一個實行的狀態。使用前面的repeat-fail迴圈結構無法完成引數的傳遞過程,因此我們就使用遞迴程式把狀態傳給它自己,而邊界條件則是到達了遊戲的最終狀態。下面的程式就是使用這種方法編制而成的。

遊戲的狀態使用列表儲存,列表的每個元素就是我們前面所定義的狀態謂詞,請看initial_state/1謂詞。而每個命令都要對這個列表有不同的操作,謂詞get_state/3, add_state/4, 和del_state/4就是完成這個任務的,它們提供了操作狀態列表的方法。

這種Prolog程式就是純邏輯的,它完全避免的使用全域性資料的麻煩。但是它需要更復雜的謂詞來操作引數中的狀態。而列表操作與遞迴程式則是最難除錯的了。至於使用哪種方法就要有你決定了。

% a nonassertive version of nani search

nani :-
write(‘Welcome to Nani Search’),
nl,
initial_state(State),
control_loop(State).

control_loop(State) :-
end_condition(State).
control_loop(State) :-
repeat,
write(’> '),
read(X),
constraint(State, X),
do(State, NewState, X),
control_loop(NewState).

% initial dynamic state

initial_state([
here(kitchen),
have([]),
location([
kitchen/apple,
kitchen/broccoli,
office/desk,
office/flashlight,
cellar/nani ]),
status([
flashlight/off,
game/on]) ]).

% static state

rooms([office, kitchen, cellar]).

doors([office/kitchen, cellar/kitchen]).

connect(X,Y) :-
doors(DoorList),
member(X/Y, DoorList).
connect(X,Y) :-
doors(DoorList),
member(Y/X, DoorList).

% list utilities

member(X,[X|Y]).
member(X,[Y|Z]) :- member(X,Z).

delete(X, [], []).
delete(X, [X|T], T).
delete(X, [H|T], [H|Z]) :- delete(X, T, Z).

% state manipulation utilities

get_state(State, here, X) :-
member(here(X), State).
get_state(State, have, X) :-
member(have(Haves), State),
member(X, Haves).
get_state(State, location, Loc/X) :-
member(location(Locs), State),
member(Loc/X, Locs).
get_state(State, status, Thing/Stat) :-
member(status(Stats), State),
member(Thing/Stat, Stats).

del_state(OldState, [location(NewLocs) | Temp], location, Loc/X):-
delete(location(Locs), OldState, Temp),
delete(Loc/X, Locs, NewLocs).

add_state(OldState, [here(X)|Temp], here, X) :-
delete(here(), OldState, Temp).
add_state(OldState, [have([X|Haves])|Temp], have, X) :-
delete(have(Haves), OldState, Temp).
add_state(OldState, [status([Thing/Stat|TempStats])|Temp],
status, Thing/Stat) :-
delete(status(Stats), OldState, Temp),
delete(Thing/
, Stats, TempStats).

% end condition

end_condition(State) :-
get_state(State, have, nani),
write(‘You win’).
end_condition(State) :-
get_state(State, status, game/off),
write(‘quitter’).

% constraints and puzzles together

constraint(State, goto(cellar)) :-
!, can_go_cellar(State).
constraint(State, goto(X)) :-
!, can_go(State, X).
constraint(State, take(X)) :-
!, can_take(State, X).
constraint(State, turn_on(X)) :-
!, can_turn_on(State, X).
constraint(_, _).

can_go(State,X) :-
get_state(State, here, H),
connect(X,H).
can_go(_, X) :-
write(‘You can’‘t get there from here’),
nl, fail.

can_go_cellar(State) :-
can_go(State, cellar),
!, cellar_puzzle(State).

cellar_puzzle(State) :-
get_state(State, have, flashlight),
get_state(State, status, flashlight/on).
cellar_puzzle(_) :-
write(‘It’‘s dark in the cellar’),
nl, fail.

can_take(State, X) :-
get_state(State, here, H),
get_state(State, location, H/X).
can_take(State, X) :-
write(‘it is not here’),
nl, fail.

can_turn_on(State, X) :-
get_state(State, have, X).
can_turn_on(_, X) :-
write(‘You don’‘t have it’),
nl, fail.

% commands

do(Old, New, goto(X)) :- goto(Old, New, X), !.
do(Old, New, take(X)) :- take(Old, New, X), !.
do(Old, New, turn_on(X)) :- turn_on(Old, New, X), !.
do(State, State, look) :- look(State), !.
do(Old, New, quit) :- quit(Old, New).
do(State, State, _) :-
write(‘illegal command’), nl.

look(State) :-
get_state(State, here, H),
write('You are in '), write(H),
nl,
list_things(State, H), nl.

list_things(State, H) :-
get_state(State, location, H/X),
tab(2), write(X),
fail.
list_things(_, _).

goto(Old, New, X) :-
add_state(Old, New, here, X),
look(New).

take(Old, New, X) :-
get_state(Old, here, H),
del_state(Old, Temp, location, H/X),
add_state(Temp, New, have, X).

turn_on(Old, New, X) :-
add_state(Old, New, status, X/on).

quit(Old, New) :-
add_state(Old, New, status, game/off).

使用這種遞迴的方法來完成任務,還有一個問題需要考慮。Prolog需要使用堆疊來儲存遞迴的一些中間資訊,當遞迴深入下去時,堆疊會越來越大。在本遊戲中,由於引數較為複雜,堆疊是很容易溢位的。

幸運的是,Prolog對於這種型別的遞迴有優化的方法。

尾遞迴

遞迴有兩種型別。在真正的遞迴程式中,每一層必須使用下一層呼叫返回的資訊。這意味著Prolog必須建立堆疊來儲存每一層的資訊。

這與重複操作是不同的,在通常的語言中,我們一般使用的是重複操作。重複操作只需要把資訊傳遞下去就行了,而不需要儲存每一次呼叫的資訊。我們可以使用遞迴來實現重複,這種遞迴就叫做尾遞迴。它的通常的形式是遞迴語句在最後,每一層的計算不需要使用下一層的返回資訊,所以在這種情況下,好的Prolog直譯器不需要使用堆疊。

計算階乘就屬於尾遞迴型別。首先我們使用通常的遞迴形式。注意從下一層返回的變數FF的值被使用到了上一層。

factorial_1(1,1).
factorial_1(N,F):-
N > 1,
NN is N - 1,
factorial_1(NN,FF),
F is N FF.

?- factorial_1(5,X).
X = 120

如果引入一個新的變數來儲存前面呼叫的結果,我們就可以把factorial/3寫成尾遞迴的形式。新的引數的初始值為1。每次遞迴呼叫將計算第二個引數的值,當到達了邊界條件,第三個引數就繫結為第二個引數。

factorial_2(1,F,F).
factorial_2(N,T,F):-
N > 1,
TT is N T,
NN is N - 1,
factorial_2(NN,TT,F).

?- factorial_2(5,1,X).
X = 120

它的結果和前面的相同,不過由於使用了尾遞迴,就不需要使用堆疊來儲存中間的資訊了。

把列表的元素順序倒過來的謂詞也可以使用尾遞迴來完成。

naive_reverse([],[]).
naive_reverse([H|T],Rev):-
naive_reverse(T,TR),
append(TR,[H],Rev).

?- naive_reverse([ants, mice, zebras], X).
X = [zebras, mice, ants]

這個謂詞在邏輯上是完全正確的,不過它的執行效率非常低。所以我們把它叫做原始(naive)的遞迴。

當引入一個用來儲存部分運算結果的新的引數後,我們就可以使用尾遞迴來重寫這個謂詞。

reverse([], Rev, Rev).
reverse([H|T], Temp, Rev) :-
reverse(T, [H|Temp], Rev).

?- reverse([ants, mice, zebras], [], X).
X = [zebras, mice, ants]