1. 程式人生 > >Prolog教程 12-列表(list)

Prolog教程 12-列表(list)

list [a,b,c]
[X|Y] X 是列表頭(head), 它可以是一個列表,或其他任何資料結構
Y 是列表尾(tail), 它只能可以是一個列表
為了能夠更好地表達一組資料,Prolog引入了列表(List)這種資料結構。 列表是一組專案的集合,此專案可以是Prolog的任何資料型別,包括結構和列表。列表的元素由方括號括起來,專案中間使用逗號分割。例如下面的列表列出了廚房中的物品。

[apple, broccoli, refrigerator]

我們可以使用列表來代替以前的多個子句。例如:

loc_list([apple, broccoli, crackers], kitchen).
loc_list([desk, computer], office).
loc_list([flashlight, envelope], desk).
loc_list([stamp, key], envelope).
loc_list([‘washing machine’], cellar).
loc_list([nani], ‘washing machine’).

可見使用列表能夠簡化程式。

當某個列表中沒有專案時我們稱之為空表,使用“[]”表示。也可以使用nil來表示。下面的句子表示hall中沒有東西。

loc_list([], hall)

變數也可以與列表聯合,就像它與其他的資料結構聯合一樣。假如資料庫中有了上面的子句,就可以進行如下的詢問。

?- loc_list(X, kitchen).
X = [apple, broccoli, crackers]

?- [,X,] = [apples, broccoli, crackers].
X = broccoli

最後這個例子可以取出列表中任何一個專案的值,但是這種方法是不切實際的。你必須知道列表的長度,但是在很多情況下,列表的長度是變化的。

為了更加有效的使用列表,必須找到存取、新增和刪除列表專案的方法。並且,我們應該不用對列表專案數和它們的順序操心。

Prolog提供的兩個特性可以方便的完成以上任務。首先,Prolog提供了把表頭專案以及除去表頭專案後剩下的列表分離的方法。其次,Prolog強大的遞迴功能可以方便地訪問除去表頭專案後的列表。

使用這兩個性質,我們可以編出一些列表的實用謂詞。例如member/2,它能夠找到列表中的元素;append/3可以把兩個列表連線起來。這些謂詞都是首先對列表頭進行處理,然後使用遞迴處理剩下的列表。

首先,請看一般的列表形式。

[X | Y]

使用此列表可以與任意的列表匹配,匹配成功後,X繫結為列表的第一個專案的值,我們稱之為表頭(head)。而Y則繫結為剩下的列表,我們稱之為表尾(tail)。

下面我們看幾個例子。

?- [a|[b,c,d]] = [a,b,c,d].
yes

上面的聯合之所以成功,是因為等號兩邊的列表是等價的。注意表尾tail一定是列表,而表頭則是一個專案,可以是表,也可以是其他的任何資料結構。下面的匹配失敗,在“|”之後只能是一個列表,而不能是多個專案。

?- [a|b,c,d] = [a,b,c,d].
no

下面是其它的一些列表的例子。

?- [H|T] = [apple, broccoli, refrigerator].
H = apple
T = [broccoli, refrigerator]

?- [H|T] = [a, b, c, d, e].
H = a
T = [b, c, d, e]

?- [H|T] = [apples, bananas].
H = apples
T = [bananas]

?- [H|T] = [a, [b,c,d]]. 這個例子中的第一層列表有兩個專案。
H = a
T = [[b, c, d]]

?- [H|T] = [apples]. 列表中只有一個專案的情況
H = apples
T = []

空表不能與[H|T]匹配,因為它沒有表頭。

?- [H|T] = [].
no

注意:最後這個匹配失敗非常重要,在遞迴過程中經常使用它作為邊界檢測。即只要表不為空,那麼它就能與[X|Y]匹配,當表為空時,就不能匹配,表示已經到達的邊界條件。

我們還可以在第二個專案後面使用“|”,事實上,|前面的都是專案,後面的是一個表。

?- [One, Two | T] = [apple, sprouts, fridge, milk].
One = apple
Two = sprouts
T = [fridge, milk]

請注意下面的例子中變數是如何與結構繫結的。內部變數現實除了變數之間的聯絡。

?- [X,Y|T] = [a|Z].
X = a
Y = _01
T = _03
Z = [_01 | _03]

這個例子中,右邊列表中的Z代表其表尾,與左邊列表中的[Y|T]繫結。

?- [H|T] = [apple, Z].
H = apple
T = [_01]
Z = _01

上面的例子中,左邊的表為T繫結為右邊的表尾[Z]。

請仔細研究最後的這兩個例子,表的聯合對編制列表謂詞是很有幫助的。

表可以看作是表頭專案與表尾列表組合而成。而表尾列表又是由同樣的方式組成的。所以表的定義本質上是遞迴定義。我們來看看下面的例子。

?- [a|[b|[c|[d|[]]]]] = [a,b,c,d].
yes

前面我們說過,列表是一種特殊的結構。最後的這個例子讓我們對錶的理解加深了。它事實上是一個有兩個引數的謂詞。第一個引數是表頭專案,第二個引數是表尾列表。如果我們把這個謂詞叫做dot/2的話,那麼列表[a,b,c,d]可以表示為:

dot(a,dot(b,dot(c,dot(d,[]))))

事實上,這個謂詞是存在的,至少在概念上是這樣,我們用“.”來表示這個謂詞,讀作dot。

我們可以使用內部謂詞display/1來顯示dot,它和謂詞write/1大致上相同,但是當它的引數為列表時將使用dot語法來顯示列表。

?- X = [a,b,c,d], write(X), nl, display(X), nl.
[a,b,c,d]
.(a,.(b,.(c,.d(,[]))))

?- X = [Head|Tail], write(X), nl, display(X), nl.
[_01, _02]
.(_01,_02)

?- X = [a,b,[c,d],e], write(X), nl, display(X), nl.
[a,b,[c,d],e]
.(a,.(b,.(.(c,.(d,[])),.(e,[]))))

從這個例子中我們可以看出為什麼不使用結構的語法來表示列表。因為它太複雜了,不過實際上列表就是一種巢狀式的結構。這一點在我們編制列表的謂詞時應該牢牢地記住。

我們可以很容易地寫出遞迴的謂詞來處理列表。首先我們來編寫謂詞member/2,它能夠判斷某個專案是否在列表中。

首先我們考慮邊界條件,即最簡單的情況。某專案是列表中的元素,如果此專案是列表的表頭。寫成Prolog語言就是:

member(H,[H|T]).

從這個子句我們可以看出含有變數的事實可以當作規則使用。

第二個子句用到了遞迴,其意義是:如果專案是某表的表尾tail的元素,那麼它也是此列表的元素。

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

完整的謂詞如下:

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

請注意兩個member/2謂詞的第二個引數都是列表。由於第二個子句中的T也是一個列表,所以可以遞迴地進行下去。

?- member(apple, [apple, broccoli, crackers]).
yes

?- member(broccoli, [apple, broccoli, crackers]).
yes

?- member(banana, [apple, broccoli, crackers]).
no

下面是member/2謂詞的單步執行結果。

我們的詢問是
?- member(b, [a,b,c]).

1-1 CALL member(b,[a,b,c])
目標模板與第一個子句不匹配,因為b不是[a,b,c]列表的頭部。但是它可以與第二個子句匹配。

1-1 try (2) member(b,[a,b,c])
第二個子句遞迴呼叫member/2謂詞。

2-1 CALL member(b,[b,c])
這時,能夠與第一個子句匹配了。

2-1 EXIT (1) member(b,[b,c])
於是一直成功地返回到我們的詢問子句。

1-1 EXIT (2) member(b,[a,b,c])
yes

和大部分Prolog的謂詞一樣,member/2有多種使用方法。如果詢問的第一引數是變數,member/2可以把列表中所有的專案找出來。

?- member(X, [apple, broccoli, crackers]).
X = apple ;
X = broccoli ;
X = crackers ;
no

下面我們將使用內部變數來跟蹤member/2的這種使用方法。請記住每一層遞迴都會產生自己的變數,但是它們之間通過模板聯合在一起。

由於第一個引數是變數,所以詢問的模板能夠與第一個子句匹配,並且變數X將繫結為表頭。回顯出X的值後,使用者使用分號引起回溯,Prolog繼續尋找更多的答案,與第二個子句進行匹配,這樣就形成了遞迴呼叫。

我們的詢問是
?- member(X,[a,b,c]).
當X=a時,目標能夠與第一個子句匹配。

1-1 CALL member(_0,[a,b,c])
1-1 EXIT (1) member(a,[a,b,c])
X = a ;
回溯時釋放變數,並且開始考慮第二條子句。

1-1 REDO member(_0,[a,b,c])
1-1 try (2) member(_0,[a,b,c])
第二層也成功了,和第一層相同。

2-1 CALL member(_0,[b,c])
2-1 EXIT (1) member(b,[b,c])
1-1 EXIT member(b,[a,b,c])
X = b ;
繼續第三層,和前面相似。

2-1 REDO member(_0,[b,c])
2-1 try (2) member(_0,[b,c])
3-1 CALL member(_0,[c])
3-1 EXIT (1) member(c,[c])
2-1 EXIT (2) member(c,[b,c])
1-1 EXIT (2) member(c,[a,b,c])
X = c ;
下面試圖找到空表中的元素。而空表不能與兩個子句中的任何一個表匹配,所以查詢失敗了。

3-1 REDO member(_0,[c])
3-1 try (2) member(_0,[c])
4-1 CALL member(_0,[])
4-1 FAIL member(_0,[])
3-1 FAIL member(_0,[c])
2-1 FAIL member(_0,[b,c])
1-1 FAIL member(_0,[a,b,c])
no

下面再介紹一個有用的列表謂詞。它能夠把兩個列表連線成一個列表。此謂詞是append/3。第一個引數和第二個引數連線的表為第三個引數。例如:

?- append([a,b,c],[d,e,f],X).
X = [a,b,c,d,e,f]

這個地方有一個小小的麻煩,因為最基本的列表操作只能取得列表的頭部,而不能在內表尾部新增專案。append/3使用遞迴地減少第一個列表長度的方法來解決這個問題。

邊界條件是:如果空表與某個表連線就是此表本身。

append([],X,X).

而遞迴的方法是:如果列表[H|T1]與列表X連線,那麼新的表的表頭為H,表尾則是列表T1與X連線的表。

append([H|T1],X,[H|T2]) :- append(T1,X,T2)

完整的謂詞就是:

append([],X,X).
append([H|T1],X,[H|T2]) :- append(T1,X,T2).

Prolog真正獨特的地方就在這裡了。在每一層都將有新的變數被繫結,它們和上一層的變數聯合起來。第二條子句的遞迴部分的第三個引數T2,與其頭部的第三個引數的表尾相同,這種關係在每一層中都是使用變數的繫結來體現的。下面是跟蹤執行的結果。

我們的詢問是:
?- append([a,b,c],[d,e,f],X).
1-1 CALL append([a,b,c],[d,e,f],_0)
X = _0
2-1 CALL append([b,c],[d,e,f],_5)
_0 = [a|_5]
3-1 CALL append([c],[d,e,f],_9)
_5 = [b|_9]
4-1 CALL append([],[d,e,f],_14)
_9 = [c|_14]
把變數的所有聯絡都考慮進來,我們可以看出,這時變數X有如下的繫結值。

X = [a|[b|[c|_14]]]
到達了邊界條件,因為第一個引數已經遞減為了空表。與第一條子句匹配時,變數_14繫結為表[d,e,f],這樣我們就得到了X的值。

4-1 EXIT (1) append([],[d,e,f],[d,e,f])
3-1 EXIT (2) append([c],[d,e,f],[c,d,e,f])
2-1 EXIT (2) append([b,c],[d,e,f],[b,c,d,e,f])
1-1 EXIT (2)append([a,b,c],[d,e,f],[a,b,c,d,e,f])
X = [a,b,c,d,e,f]

和member/2一樣,append/3還有別的使用方法。下面這個例子顯示了append/3是如何把一個表分解的。

?- append(X,Y,[a,b,c]).
X = []
Y = [a,b,c] ;

X = [a]
Y = [b,c] ;

X = [a,b]
Y = [c] ;

X = [a,b,c]
Y = [] ;
no

使用列表

現在有了能夠處理列表的謂詞,我們就可以在遊戲中使用它們。例如使用謂詞loc_list/2代替原來的謂詞location/2來儲存物品,然後再重新編寫location/2來完成與以前同樣的操作。只不過是以前是通過location/2尋找答案,而現在是使用location/2計算答案了。這個例子從某種程度上說明了Prolog的資料與過程之間沒有明顯的界限。無論是從資料庫中直接找到答案,或是通過一定的計算得到答案,對於呼叫它的謂詞來說都是一樣的。

location(X,Y):- loc_list(List, Y), member(X, List).

當某個物品被放入房間時,需要修改此房間的loc_lists,我們使用append/3來編寫謂詞add_thing/3:

add_thing(NewThing, Container, NewList):-
loc_list(OldList, Container),
append([NewThing],OldList, NewList).

其中,NewThing是要新增的物品,Container是此物品的位置,NewList是新增物品後的列表。

?- add_thing(plum, kitchen, X).
X = [plum, apple, broccoli, crackers]

當然,也可以直接使用[Head|Tail]這種列表結構來編寫add_thing/3。

add_thing2(NewThing, Container, NewList):-
loc_list(OldList, Container),
NewList = [NewThing | OldList].

它和前面的add_thing/3功能相同。

?- add_thing2(plum, kitchen, X).
X = [plum, apple, broccoli, crackers]

我們還可以對add_thing2/3進行簡化,不是用顯式的聯合,而改為在子句頭部的隱式聯合。

add_thing3(NewTh, Container,[NewTh|OldList]) :-
loc_list(OldList, Container).

它同樣能完成我們的任務。

?- add_thing3(plum, kitchen, X).
X = [plum, apple, broccoli, crackers]

下面的put_thing/2,能夠直接修改動態資料庫,請自己研究一下。

put_thing(Thing,Place) :-
retract(loc_list(List, Place)),
asserta(loc_list([Thing|List],Place)).

到底是使用多條子句,還是使用列表方式,這完全有你的程式設計習慣來決定。有時使用Prolog的自動回溯功能較好,而有時則使用遞迴的方式較好。還有些較為複雜的情況,需要同時使用子句和列表來表達資料。 這就必須掌握兩種資料表達方式之間的轉換。

把一個列表轉換為多條子句並不難。使用遞迴過程逐步地把表頭asserts到資料庫中就行了。下面的例子把列表轉化為了stuff的一系列子句。

break_out([]).
break_out([Head | Tail]):-
assertz(stuff(Head)),
break_out(Tail).

?- break_out([pencil, cookie, snow]).
yes

?- stuff(X).
X = pencil ;
X = cookie ;
X = snow ;
no

把多條事實轉化為列表就困難多了。因此Prolog提供了一些內部謂詞來完成這個任務。最常用的謂詞是findall/3,它的引數意義如下:

引數1: 結果列表的模板。
引數2: 目標模板。
引數3: 結果列表。

findall/3自動地尋找目標,並把結果儲存到一個列表中。使用它可以方便的把stuff子句還原成列表。

?- findall(X, stuff(X), L).
L = [pencil, cookie, snow]

下面把所有與廚房相連的房間找出來。

?- findall(X, connect(kitchen, X), L).
L = [office, cellar, ‘dining room’]

最後我們再來看一個複雜的例子:

?- findall(foodat(X,Y), (location(X,Y) , edible(X)), L).
L = [foodat(apple, kitchen), foodat(crackers, kitchen)]

它找出了所有能吃的東西及其位置,並把結果放到了列表中。