1. 程式人生 > >Prolog教程 6--規則

Prolog教程 6--規則

pred(x):- pred(Y),pred(Z).

前面我們已經說過,謂詞是使用一系列的子句來定義的。以前我們所學習的子句是事實,現在讓我們來看看規則吧。規則的實質就是儲存起來的查詢。它的語法如下:

head :- body

其中,

head 是謂詞的定義部分,與事實一樣,也包括謂詞名和謂詞的引數說明。
:- 連線符,一般可以讀作‘如果’。
body 一個或多個目標,與查詢相同。

舉個例子,上一章中的混合查詢–找到能吃的東西和它所在的房間,可以使用如下的規則儲存,規則名為where_food/2。

where_food(X,Y) :- location(X,Y), edible(X).

用語言來描述就是“在房間Y中有可食物X的條件是:X在Y房間中,並且X可食。”

我們現在可以直接使用此規則來找到房間中可食的物品。

?- where_food(X, kitchen).
X = apple ;
X = crackers ;
no

?- where_food(Thing, ‘dining room’).
no

它也可以用來判斷,

?- where_food(apple, kitchen).
yes

或者通過它找出所有的可食物及其位置,

?- where_food(Thing, Room).
Thing = apple
Room = kitchen ;

Thing = crackers
Room = kitchen ;
no

我們可以使用多個事實來定義一個謂詞,同樣我們也可以用多個規則來定義一個謂詞。例如,如果想讓Prolog知道broccoli(椰菜)也是可食物,我們可以如下定義where_food/2規則。

where_food(X,Y) :- location(X,Y), edible(X).
where_food(X,Y) :- location(X,Y), tastes_yucky(X).

在以前的事實中我們沒有把broccoli定義為edible,即沒有edible(broccoli).這個事實,所以單靠where_food的第一個子句是不能找出broccoli的,但是我們曾在事實中定義過:tastes_yucky(broccoli).{不好吃(椰菜).},所以如果加入第二個子句,Prolog就可以知道broccoli也是food(食物)了。下面是它的執行結果。

?- where_food(X, kitchen).
X = apple ;
X = crackers ;
X = broccoli ;
no

規則的工作原理

到現在為止,我們所知道的Prolog所搜尋的子句只有事實。下面我們來看看Prolog是如何搜尋規則的。

首先,Prolog將把目標和規則的子句的頭部(head)進行匹配,如果匹配成功,Prolog就把此規則的body部分作為新的目標進行搜尋。

實際上規則就是多層的詢問。第一層由原始的目標組成,從下一層開始就是由與第一層的目標相匹配的規則的Body中的子目標組成。(這句話有點難理解,請參照下面圖來分析)

每一層還可以有子目標,理論上來講,這種目標的巢狀可以是無窮的。但是由於計算機的硬體限制,子目標只可能有有限次巢狀。

下圖顯示了這種目標巢狀的流程圖,請你注意第一層的第三個目標是如何把控制權回溯到第二層的子目標中的。

在這個例子中,第一層的中間的那個目標的結果依賴於第二層的目標的結果。此目標會把程式的控制權傳給他的子目標。

下面我們詳細地分析一下Prolog在匹配有規則的子句時是如何工作的。請注意用‘-’分隔的兩個數字,第一個數字代表當前的目標級數,第二個數字代表當前目標層中正在匹配的目標的序號。例如:

2-1 EXIT (7) location(crackers, kitchen)

表示第二層的第一個目標的EXIT過程。

我們的詢問如下
?- where_food(X, kitchen).
首先我們尋找有where_food/2的子句.
1-1 CALL where_food(X, kitchen)
與第一個子句的頭匹配
1-1 try (1) where_food(X, kitchen) ;第一個where_food/2的子句與目標匹配。
於是第一個子句的Body將變為新的目標。
2-1 CALL location(X, kitchen)
從現在起的執行過程就和我們以前一樣了。
2-1 EXIT (2) location(apple, kitchen)
2-2 CALL edible(apple)
2-2 EXIT (1) edible(apple)
由於Body的所有目標都成功了,所以第一層的目標也就成功了。
1-1 EXIT (1) where_food(apple, kitchen)
X = apple ;
第一層的回溯過程使得又重新進入了第二層的目標。
1-1 REDO where_food(X, kitchen)
2-2 REDO edible(apple)
2-2 FAIL edible(apple)
2-1 REDO location(X, kitchen)
2-1 EXIT (6) location(broccoli, kitchen)
2-2 CALL edible(broccoli)
2-2 FAIL edible(broccoli)
2-1 REDO location(X, kitchen)
2-1 EXIT (7) location(crackers, kitchen)
2-2 CALL edible(crackers)
2-2 EXIT (2) edible(crackers)
1-1 EXIT (1) where_food(crackers, kitchen)
X = crackers ;
下面就沒有更多的答案了,於是第一層的目標失敗。
2-2 REDO edible(crackers)
2-2 FAIL edible(crackers)
2-1 REDO location(X, kitchen)
2-1 FAIL location(X, kitchen)

下面Prolog開始尋找另外的子句,看看它們的頭部(head)能否與目標匹配。在此例中,where_food/2的第二個子句也可以與詢問匹配。
1-1 REDO where_food(X, kitchen)
Prolog又開始試圖匹配第二個子句的Body中的目標。
1-1 try (2) where_food(X, kitchen) ;第二個where_food/2的子句與目標匹配。
下面將找到不好吃的椰菜。即 tastes_yucky 的 broccoli.
2-1 CALL location(X, kitchen)
2-1 EXIT (2) location(apple, kitchen)
2-2 CALL tastes_yucky(apple)
2-2 FAIL tastes_yucky(apple)
2-1 REDO location(X, kitchen)
2-1 EXIT (6) location(broccoli, kitchen)
2-2 CALL tastes_yucky(broccoli)
2-2 EXIT (1) tastes_yucky(broccoli)
1-1 EXIT (2) where_food(broccoli, kitchen)
X = broccoli ;
回溯過程將讓Prolog尋找另外的where_food/2的子句。但是,這次它沒有找到。
2-2 REDO tastes_yucky(broccoli)
2-2 FAIL tastes_yucky(broccoli)
2-1 REDO location(X,kitchen)
2-1 EXIT (7) location(crackers, kitchen)
2-2 CALL tastes_yucky(crackers)
2-2 FAIL tastes_yucky(crackers)
2-2 REDO location(X, kitchen)
2-2 FAIL location(X, kitchen)
1-1 REDO where_food(X, kitchen) ;沒有找到更多的where_food/2的子句了。
1-1 FAIL where_food(X, kitchen)
no

在詢問的不同層的目標中,即是相同的變數名稱也是不同的變數,因為它們都是區域性變數。這於其他語言中的區域性變數是差不多的。

我們再來分析一下上面的那個例子吧。

where_food(X,Y) :- location(X,Y), edible(X).

查詢的目標是:

?- where_food(X1, kitchen)

第一個子句的頭是:

where_food(X2, Y2)

目標和子句的頭部匹配,在Prolog中如果變數和原子匹配,那麼變數就繫結為此原子的值。如果兩個變數進行了匹配,那麼這兩個變數將同時繫結為一個內部變數。此後,這兩個變數中只要有一個繫結為了某個原子的值,另外一個變數也會同時繫結為此值。所以上面的匹配操作將有如下的繫結。

X1 = _01 ;01為Prolog的內部變數。
X2 = _01
Y2 = kitchen

於是當上述的匹配操作完成後,規則where_food/2的body將變成如下的查詢:

location(_01, kitchen), edible(_01).

當內部變數取某值時,例如’apple’,X1和X2將同時繫結為此值,這是Prolog變數和其他語言的變數的基本的區別。如果你學過C語言,容易看出,實際上X1和X2都是指標變數,當它們沒有繫結值時,它們的值為NULL,一旦繫結,它們就會指向某個具體的位置,上例中它們同時指向了_01這個變數,其實_01變數還是個指標,直到最後某個指標指向了具體的值,那麼所有的指標變數就都被繫結成了此值。

使用規則

使用規則我們可以很容易的解決單向門的問題。我們可以再定義有兩個子句的謂詞來描述這種雙向的聯絡。此謂詞為connect/2。

connect(X,Y) :- door(X,Y).
connect(X,Y) :- door(Y,X).

它代表的意思是“房間X和Y相連的條件是:從X到Y有扇門,或者從Y到X有扇門"。請注意此處的或者, 為了描述這種或者的關係我們可以為某個謂詞定義多個子句。

?- connect(kitchen, office).
yes

?- connect(office, kitchen).
yes

我們還可以讓Prolog列出所有相連的房間。

?- connect(X,Y).
X = office
Y = hall ;

X = kitchen
Y = office ;


X = hall
Y = office ;

X = office
Y = kitchen ;

使用我們現在所具有的知識,我們可以為“搜尋Nani”加入更多的謂詞。首先我們定義look/0,它能夠顯示玩家所在的房間,以及此房間中的物品和所有的出口。

先定義list_things/1,它能夠列出某個房間中的物品。

list_things(Place) :-
location(X, Place),
tab(2),
write(X),
nl,
fail.

它和上一章中的最後一個例子差不多。我們可以如下使用它。

?- list_things(kitchen).
apple
broccoli
crackers
no

這地方有一個小問題,它雖然把所有的東西都列出來了,但是最後那個no不太好看,並且如果我們把它和其他的規則連起來用時麻煩就更大了,因為此規則的最終結果都是fail。實際上它是我們擴充的I/O謂詞,所以它應該總是成功的。我們可以很容易的解決這個問題。

list_things(Place) :-
location(X, Place),
tab(2),
write(X),
nl, fail.
list_things(AnyPlace).

如上所示,加入list_things(AnyPlace)子句後就可以解決了,第一個list_things/1的子句把所有的物品列出,並且失敗,而由於第二個子句永遠是成功的,所以list_things/1也將是成功的。AnyPlace變數的值我們並不關心,所以我們可以使用無名變數‘_’來代替它。

list_things(_).

下面我們來編寫list_connections/1,它能夠列出與某個房間相連的所有房間。

list_connections(Place)
:- connect(Place, X),
tab(2),
write(X),
nl,
fail.

list_connections(_).

我們來試試功能,

?- list_connections(hall).
dining
room
office
yes

終於可以來編寫look/0了,

look :-
here(Place),
write('You are in the '),
write(Place),
nl,
write(‘You can see:’),
nl,
list_things(Place),
write(‘You can go to:’),
nl,
list_connections(Place).

在我們定義的事實中有here(kitchen).它代表玩家所在的位置。以後我們將學習如何改變此事實。現在來試是功能吧,

?- look.
You are in the kitchen
You can see:
apple
broccoli
crackers
You can go to:
office
cellar
dining
room
yes