1. 程式人生 > >Prolog教程 15

Prolog教程 15

在前面的章節中,我們瞭解了Prolog是如何解釋目標的,並且通過例項說明了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]
 

相關推薦

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

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

Prolog教程 15

在前面的章節中,我們瞭解了Prolog是如何解釋目標的,並且通過例項說明了Prolog的執行流程。在這一章,繼續探索Prolog的程式流程控制,我們將介紹和一般的程式設計語言相似的流程控制。前面我們使用謂詞fail和write/1來列印出遊戲中所有的物品。這種流程控制類似於

JPA 菜鳥教程 15 繼承-一個表-SINGLE_TABLE

column turn rate pre school fill 技術 一個表 tor 原地址:http://blog.csdn.net/JE_GE/article/details/53678422 繼承映射策略 一個類繼承結構一個表的策略,最終只生成一個表,這是繼承映射的

Win2012R2 Hyper-V初級教程15 -- 基於Kerberos與CA證書的系統容災(中)

系統/運維 Windows 二、基於CA證書的HTTPS復制 ??????? 剛剛看了一下關於基於Kerberos與CA證書的系統容災(上)還是2017-08-31寫的,到現在半年過去了,懶癌太重了,一直沒有更新,從今天起將逐步更新完初級教程,希望能夠有更多的博友了解並學習微軟的虛擬化技術。前面我們說

Win2012R2 Hyper-V初級教程15 -- 基於Kerberos與CA證書的系統容災(下)

系統/運維 Windows 上一文已經講到了基於CA證書下的HTTPS復制,這裏我們簡單測試一下故障轉移是否正常。 三、測試故障轉移 1、為了驗證此功能是否真的有效,我們需要對其進行一次測試,下面我們對本身沒有123.txt的桌面添加此文件,然後我們再來看一下在同步復制完成後,此功能是否生效。 2、

OGL(教程15)——攝像機控制2

原文地址:http://ogldev.atspace.co.uk/www/tutorial15/tutorial15.html 背景知識: 本節我們將會完成實現滑鼠控制攝像機方向。在涉及攝像機的時候有很多不能層級的自由性。我們將以第一視角遊戲的方式控制相機。這就意味著我們能夠改變攝像機的

django系列教程15:資料庫遷移

目錄 1.遷移 1.生成遷移檔案 2.執行遷移命令 新增測試資料 1.遷移 建立完模型類後,並沒有真正的在資料庫中建立了資料表,需要執行遷移命令,在資料表中建立資料表。 1.生成遷移檔案 manage.py 檔案所在目錄執行命令 python manag

django系列教程15:數據庫遷移

man 是否 val 需要 天龍八部 數據 del delete 查看 目錄 1.遷移 1.生成遷移文件 2.執行遷移命令 添加測試數據 1.遷移 創建完模型類後,並沒有真正的在數據庫中創建了數據表,需要執行遷移命令,在數據表中創建數據表。 1.生成遷移文件 mana

Java Servlet 實戰入門教程-15-servlet 異常處理

異常處理 請求屬性 在發生錯誤時,Web 應用程式必須能夠詳細說明,應用程式中的其他資源被用來提供錯誤響應的內容主體。這些資源的規定在部署描述檔案中配置。 如果錯誤處理位於一個servlet或JSP頁面: 原來開啟的由容器建立的請求和響應物件被傳遞給servle

python教程15、網路程式設計之socket,socketserver,select,twisted

一、TCP/IP相關知識 TCP/UDP提供程序地址,兩個協議互不干擾的獨自的協議       TCP :Transmission Control Protocol 傳輸控制協議,面向連線的協議,通訊前需要建立通訊通道(虛擬鏈路),結束後拆除鏈路

Python爬蟲入門教程 15-100 石家莊政民互動資料爬取

寫在前面 今天,咱抓取一個網站,這個網站呢,涉及的內容就是 網友留言和回覆,特別簡單,但是網站是gov的。網址為 http://www.sjz.gov.cn/col/1490066682000/index.html 首先宣告,為了學習,絕無惡意抓取資訊,不管

Golang教程15節--指標

什麼是指標? 指標是一種儲存變數記憶體地址的變數。 如上圖所示,變數 b 的值為 156,而 b 的記憶體地址為 0x1040a124。變數 a 儲存了 b 的地址。我們就稱 a 指向了 b。 指標的宣告 指標變數的型別為 *T,該指標指向一個 T 型別的變數

Java基礎教程(15)--列舉型別

  列舉型別定義了一個列舉值的列表,每個值是一個識別符號。例如,下面的語句聲明瞭一個列舉型別,用來表示星期的可能情況: public enum Day { SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY }   實際上,

講給Android程式設計師看的前端教程(15)——CSS選擇器(5)

在之前的幾篇部落格中分別介紹了CSS中常用的選擇器:標籤選擇器,類選擇器,ID選擇器,萬用字元選擇器。在本篇部落格中,我們將學習這些選擇器的複合使用。 並集選擇器 並集選擇器由各個選擇器通過逗號連線而成的,它為不同的標籤設定相同的CSS樣式;語

Prolog教程 1

(轉)Prolog教程 1 今天是2019年1月1日, 因為AI課要用到Prolog, 看到不錯的入門教程收錄下來學習。 如果你是一位prolog的新手,希望你首先閱讀這篇文章,好對prolog的全域性有個瞭解。在這篇文章中我會把prolog和其他的程式語言做比較,所以希望你已經具有

Prolog教程 2

Prolog在英語中的意思就是Programming in LOGic(邏輯程式設計)。它是建立在邏輯學的理論基礎之上的, 最初是運用於自然語言的研究領域。然而現在它被廣泛的應用在人工智慧的研究中,它可以用來建造專家系統、自然語言理解、智慧知識庫等。同時它對一些通常的應用程式的編寫也很有幫助

Prolog教程 14--cut的功能

cut,使用符號!來表示。 直到目前為止,我們都一直在使用Prolog內建的回溯功能。使用此功能可以方便地寫出結構緊湊的謂詞來。 但是,並不是所有的回溯都是必須的,這時我們需要能夠人工地控制回溯過程。Prolog提供了完成此功能的謂詞,他叫做cut,使用符號!來表示。 Cut能夠有效

Prolog教程 13--定義操作符

我們已經學習過了Prolog的資料結構,它的形式如下: functor(arg1,arg2,…,argN). 這是Prolog的唯一的資料結構,但是Prolog允許這種資料結構有其它的表達方法(僅僅是表達方法不同)。這種表達方法有時候更加接近我們的習慣,正如列表的兩種表達法一樣。現在

Prolog教程 12-列表(list)

list [a,b,c] [X|Y] X 是列表頭(head), 它可以是一個列表,或其他任何資料結構 Y 是列表尾(tail), 它只能可以是一個列表 為了能夠更好地表達一組資料,Prolog引入了列表(List)這種資料結構。 列表是一組專案的集合,此專案可以是Prolog的任何資料型別

Prolog教程 11-聯合(Unification)

匿名變數(_)不會繫結為任何值 如果使用(=)那麼聯合操作時顯式的。而Prolog在使用子句與目標匹配時的聯合則是隱式的。 Prolog的最強大的功能之一就是它內建了模式匹配的演算法----聯合(Unification)。以前我們所介紹的例子中的聯合都是較為簡單的。現在來仔細研究一下聯合。下