Prolog入門教程(完整版)

雷炎氏發表於2020-11-05

Prolog入門教程

在網上找了很多prolog的教程,大多數都是轉載自一處,但都是一部分,先找到原著完整版,並新增自己的使用過程,供大家參考。

前記

如果你是一位 prolog 的新手,希望你首先閱讀這篇文章,好對 prolog 的全域性有個瞭解。在這篇文章中我會把 prolog 和其他的程式語言做比較,所以希望你已經具有了一定的程式設計水平。

什麼是 prolog ?

prolog 是 Programming in LOGic 的縮寫,意思就是使用邏輯的語言編寫程式。 prolog 不是很高深的語言,相反,比較起其他的一些程式語言,例如 c 、 basic 等等語言, prolog 是更加容易理解的語言。如果你從來沒有接觸過計算機程式設計,那麼恭喜你,你將很容易的進入 prolog 世界。如果你已經是其他語言的高手,你就需要完全丟棄你原來的程式設計思路,否則是很難掌握 prolog 的。一個例子邏輯思維在我們日常生活中比比皆是, prolog 正是把這種思維用文字描述出來的計算機語言。
還是首先舉個例子吧。
比如一群年輕人正在戀愛,每個人都有自己心中所追求的物件:
張學友愛王菲
張學友愛周慧敏
王菲愛謝廷峰
周慧敏愛張學友
謝廷峰愛王菲
謝廷峰愛周慧敏
劉德華愛周慧敏

我們說兩個年輕人要互相都喜愛,他們就算是一對情侶,那麼上面的誰和誰是情侶呢?這應該算是一道最簡單邏輯推理題目了,那麼我們如何用 prolog 語言實現呢?“張學友愛王菲”是一條已知的事實,
用 prolog 語言來表達就是:
愛 ( 張學友 , 王菲 ).

注意 1 :這裡是為了閱讀方便才使用漢字的,真正的 prolog 是不允許使用除了基本字元以外字元的,也就是說,上面的句子必須寫成 love(zhangxueyou,wanfei). ,電腦才能夠真正的理解。

注意 2 :最末尾的“ . ”一定不能掉,它表示一個句子結束。

注意 3 :上面詞彙對於電腦來說並沒有真正的含義,所以我們完全可以用 ai(zxy,wf). 來表達這個關係,更進一步,我們甚至可以用 xxx(a,b). 來表達,只要你自己心裡清楚 xxx 表示愛, a 表示張學友, b 表示王菲就可以了。

注意 4 :張學友和王菲的順序也沒有特別的規定,你完全可以把他們換個位置:愛 ( 王菲 , 張學友 ). 只要你心裡清楚它表達的意思就行了,而以後都遵循這種被愛的人在前面的順序,就不會出錯。

其他的事實我就不寫了,你可以參照上面的例子自己把已知事實翻譯成 prolog 的語句。

那麼情侶的概念怎麼定義呢?也很簡單!

情侶 ( 某人甲 , 某人乙 ):- 愛 ( 某人甲 , 某人乙 ), 愛 ( 某人乙 , 某人甲 ).

:- 在 prolog 中表示“如果”的意思,我們使用它來定義規則。

上面這句話的意思就是,某人甲和某人乙是情侶的規則就是:某人甲愛某人乙,並且某人乙愛某人甲。上面用來分隔兩個愛的句子的“,”表示並且的意思。

當然為了能夠讓電腦執行,這個句子要改為英文的: lovers(X,Y):-love(X,Y),love(Y,X).

注意:在 prolog 中以小寫字元開頭的字串代表確知的事物,比如 love 表示愛這種關係,而 zhangxueyou 表示張學友。而以大寫字母開頭的字串表示未確定的事物,翻譯成漢語就是某某。

完整的可執行的 prolog 程式如下:(我的拼音不好,要是什麼人的名字拼寫錯了,請原諒:)
love(zhangxueyou,wanfei).
love(zhangxueyou,zouhuimin).
love(wanfei,xietinfen).
love(zouhuimin,zhangxueyou).
love(xietinfen,wanfei).
love(xietinfen,zouhuimin).
love(liudehua,zouhuimin).
lovers(X,Y):-love(X,Y),love(Y,X).

我們可以看出來,完整的 prolog 程式是有事實和規則組成的。事實用來儲存一些資料,而規則用來儲存某種可以推理出來的關係。

如果把上面的程式調入 prolog 直譯器(關於 prolog 直譯器,在後面有介紹)然後就可以對以上的程式進行詢問。

prolog 直譯器的提示符號為“ ?- ”,你只需要在這個提示符後面輸入自己的句子就可以了。

讓我們來看第一個詢問:

?-love(zhangxueyou,wanfei).

事實上我們的詢問完全和程式中的第一條事實一樣,這個詢問是“是非”詢問,也就是說電腦回答的答案是 yes 或者 no 。

上面的詢問的含義是:就你所知,張學友愛王菲麼?

由於我們的程式中間有這樣的事實,所以直譯器將回答。yes.

如果我們問:

?-love(zhangxueyou,liudehua).

直譯器將回答 no.

因為它沒有發現 love(zhangxueyou,liudehua). 這個事實。

在詢問中我們可以使用大寫字母代表未知的事物,讓直譯器找到答案。
例如:

?-love(zhangxueyou,X).

這句話詢問的是:張學友都喜歡哪些人。

直譯器將給出答案:

X=wanfei ;
X=zouhuimin ;
no.

注意 1 :上面的兩個“ ; ”是人工輸入的,當直譯器找到一個答案之後,它將這個答案輸出,並且等待使用者的進一步輸入,如果使用者輸入“;”,直譯器將繼續尋找其他的答案,如果輸入的是別的符號,直譯器將終止查詢。
最後那個 no. 是因為系統在輸出了 zouhuimin 這個答案以後,使用者輸入“;”,表示還想知道其他的答案,而直譯器又找不到其他的答案了,於是輸出 no. 來終止查詢。

我們再看一個例子:

?-love(X,zouhuimin).
X=zhangxueyou;
X=xietinfen;
X=liudehua;
no.

在上面的詢問中,我們只涉及到對事實的查詢,下面我們來看規則的用法。

?- lovers(X,Y).
X = zhangxueyou Y = zouhuimin ;
X = wanfeiY = xietinfen ;
X = zouhuiminY = zhangxueyou ;
X = xietinfenY = wanfei ;
no

我們看到 lovers(X,Y). 找出了系統中所有的戀人。

不過每對戀人被顯示了兩次,這是因為 prolog 是考慮順序的,也就是說 lovers(a,b). 和 lovers(b,a). 並不等價。

這一點在後面的學習中,你會了解。

再看一個例子:

?- lovers(wanfei,Y).
Y = xietinfen ;
no

詢問王菲的戀人,結果是 xietinfen 。

呵呵,還挺聰明的。我們看到同樣是 lovers ,根據其引數不同,功能也不同,這也是 prolog 的一個大特點。

最後讓我們編寫一個尋找情敵的規則來結束這一節內容吧。

rival_in_love(X,Y):-love(X,Z),not(love(Z,X)),love(Z,Y).

這段程式可以理解為:

Y 是 X 的情敵的條件是: X 喜歡 Z( 代表某個人 ) ,而 Z 不喜歡 X ,而 Y 是 Z 喜歡的人。哈哈,這不正是情敵的條件嘛。

?- rival_in_love(X,Y).
X = zhangxueyouY = xietinfen ;
X = xietinfenY = zhangxueyou ;
X = liudehuaY = zhangxueyou ;
no

好了,你自己分析一下為什麼會是這樣的答案吧。

為什麼要 prolog

看完上面的例子,不知道是否提起了你對 prolog 的興趣。如果你感興趣的話,那麼讓我們繼續來看 prolog 能夠做一些什麼事情吧。

理論上來說使用 c 語言可以編制任何種類的程式,甚至連 prolog 語言都是使用 c 語言編寫的。不過對於急於開發應用程式的使用者,最關心的是如何最經濟最有效率的開發程式, prolog 為你多提供了一個選擇的餘地。

prolog很適合於開發有關人工智慧方面的程式,例如:專家系統、自然語言理解、定理證明以及許多智力遊戲。曾經有人預言prolog將成為下一代計算機的主要語言,雖然這個夢想目前還很難實現,不過世界上已經有許多prolog的應用例項了。你要堅信,它絕對不是那種只在實驗室發揮作用的語言,之所以大多數人都不瞭解它,是因為它的應用範圍比較特殊而已。

prolog 有許多不足之處,但是這並不影響它在邏輯推理方面的強大功能,不過最好的方法是使用某種一般語言和 prolog 結合,一般語言完成計算、介面之類的操作,而 prolog 則專心實現邏輯運算的操作。

例如:你編寫一個下棋程式,用 prolog 來讓電腦思考如何下棋,而用 Visual Basic 來編寫介面。我們將在以後介紹這方面的技術。

總之, prolog 在許多方面將極大的減少你的程式設計負擔,所以趕快來了解一下它吧, 也許你日後遇到什麼難題,可以使用 prolog 迎刃而解,到那個時候,你就知道今天的學習沒有白費了。

prolog 的特點

我個人總結了 prolog 的以下幾個特點,因為叫做特點,所以自然要和其他的語言進行比較。

  1. prolog 程式沒有特定的執行順序,其執行順序是由電腦決定的,而不是程式設計序的人。從這個意義上來說, prolog 程式不是真正意義上的程式。所謂程式就是按照一定的步驟執行的計算機指令,而 prolog 程式的執行步驟不由人來決定。它更像一種描述型的語言,用特定的方法描述一個問題,然後由電腦自動找到這個問題的答案。舉個極端的例子,你只需要把某個數學題目告訴它,它就會自動的找到答案,而不像使用其他的語言一樣,必須人工的編制出某種演算法。

  2. prolog 程式中沒有 if 、 when 、 case 、 for 這樣的控制流程語句前面已經說了,程式的執行方式有電腦自己決定,當然就用不到這些控制流程的語句了。通常情況下,程式設計師不需要了解程式的執行過程,只需要注重程式的描述是否全面,不過 prolog 也提供了一些控制流程的方法,這些方法和其他語言中的方法有很大的區別,希望你在以後的學習當中能夠融會貫通。

  3. prolog 程式和資料高度統一在 prolog 程式中,是很難分清楚哪些是程式,哪些是資料的。事實上, prolog 中的所有東西都有相同的形式,也就是說資料就是程式,程式就是資料。舉一個其他語言的例子:如果想用 c 語言編寫一個計算某個數學表示式的程式很簡單 ( 比如: a=2+54) ,因為這是一段程式。但是如果想編寫一個計算使用者輸入的表示式的值的程式就很困難了。因為使用者輸入的是一段資料 ( 字串 ) ,如果想讓 c 語言處理這個字串,就需要很多方面的技術。則正是因為在 c 語言中,程式和資料是分開的。而在 prolog 就不存在這個問題,你甚至可以很輕鬆的編寫處理其它 prolog 程式的程式。

  4. prolog 程式實際上是一個智慧資料庫 prolog 的原理就是關聯式資料庫,它是建立在關聯式資料庫的基礎上的。在以後的學習中你會發現它和 SQL 資料庫查詢語言有很多相似之處。使用 prolog 可以很方便的處理資料。

  5. 強大的遞迴功能。在其它的語言中,你也許已經接觸過遞迴程式了。遞迴是一種非常簡潔的方式,它能夠有效的解決許多難題。而在 prolog 中,遞迴的功能得到了充分的體現,你甚至都會感到驚奇,遞迴居然又如此巨大的能力。

下一步該怎麼做

如果你決定下來要學習 prolog 了,那麼請繼續看這裡的教程。你要注意哦,這裡是目前全球唯一的詳細介紹 prolog 的中文網站。

  1. 在學習之前,希望你能夠搞到比較好的 prolog 直譯器,下一節我將就直譯器進行一些討論。

  2. 然後你必須熟練的掌握直譯器的使用方法。

  3. 然後就可以開始閱讀我的教程了。

  4. 當你學習完整個教程以後,希望你能夠進入人工智慧例項環節,那裡有更多的、更有用的 prolog 程式設計方法,和有趣的程式。

  5. 如果你想使用 prolog 和其它的語言結合起來,編寫讓人瞠目結舌的又聰明、又漂亮的程式,你就應該仔細研究 VB+prolog 這一節。

好了,還等什麼? 讓我們開始吧。

prolog直譯器(新增)

因為自己使用ubuntu,這裡以linux版本為例:其他版本

本人選用SWI-prolog直譯器。

% sudo apt-add-repository ppa:swi-prolog/stable
% sudo apt-get update
% sudo apt-get install swi-prolog

按照上面的命令就可以安裝完成SWI-prolog直譯器了。

如何使用?

命令列輸入prolog即可進入直譯器模式了。
在這裡插入圖片描述

編寫事實和規則

方式一:直接寫入
[user].
方式二:檔案匯入
[‘file.pl’].

栗子:
我們以尋找爺爺為例:

grandfather(X,Z) :- father(X,Y),father(Y,Z).  // 規則
father(tom, jerry).  // 事實
father(jack, tom).  // 事實

在這裡插入圖片描述
輸入完規則和事實後,ctrl+c,再按b,退出,就可以詢問了。

問:誰是jerry的爺爺

語句:

?- grandfather(X, jerry)

答:

X = jack

同樣,匯入檔案:

在這裡插入圖片描述

探索 Prolog

Prolog 在英語中的意思就是 Programming in logic (邏輯程式設計)。它是建立在邏輯學的理論基礎之上的, 最初是運用於自然語言的研究領域。然而現在它被廣泛的應用在人工智慧的研究中,它可以用來建造專家系統、自然語言理解、智慧知識庫等。同時它對一些通常的應用程式的編寫也很有幫助。使用它能夠比其他的語言更快速地開發程式,因為它的程式設計方法更象是使用邏輯的語言來描述程式。
從純理論的角度來講, Prolog 是一種令人陶醉的程式語言,但是在這本書中還是著重介紹他的實際使用方法。

進入 Prolog 世界

和其他的語言一樣,最好的學習方法是實踐。這本書將使用 Prolog 的直譯器來向大家介紹幾個具體的應用程式的編寫過程。
首先你應該擁有一個 Prolog 的直譯器,你可以在免費 prolog 版本中找到它。

關於直譯器的使用,請參閱相關的使用說明文件,建議使用 amzi prolog 或者 swi prolog 來執行本網站的程式。

邏輯程式設計

什麼叫邏輯程式設計?也許你還沒有一個整體的印象,還是讓我們首先來研究一個簡單的例子吧。

運用經典的邏輯理論,我們可以說“所有的人( person )都屬於人類( moral )”,如果用 Prolog 的語言來說就是
“對於所有的 X ,只要 X 是一個人,它就屬於人類。”

moral(X):-person(X).

同樣,我們還可以加入一些簡單的事實,比如:蘇格拉底( socrates )是一個人。

person(socrates).

有了這兩條邏輯宣告, Prolog 就可以判斷蘇格拉底是不是屬於人類。

在 Prolog 的 Listener 中鍵入如下的命令:

?-mortal(socrates). ( 此句中的 ‘?-’ 是 Listener 的提示符,本句表示詢問蘇格拉底是不是屬於人類。)

Linstener 將給出答案:

yes

我們還可以詢問,“誰屬於人類?”

?-mortal(X).

我們會得到如下的答案:

X= socrates

這個簡單的例子顯示了 Prolog 的一些強大的功能。它能讓程式程式碼更簡潔、更容易編寫。在多數情況下 Prolog 的程式設計師不需要關心程式的執行流程,這些都由 Prolog 自動地完成了。

當然,一個完整的程式不能只包括邏輯運算部分,還必須擁有輸入輸出,乃至使用者介面部分。

很遺憾, Prolog 在這些方面做得不好,或者說很差。不過它還是提供了一些基本的方法的。

下面是上述的程式一個完整的例子。

% This is the syntax for comments. 
% MORTAL - The first illustrative Prolog program 
mortal(X) :- person(X).
person(socrates).
person(plato).
person(aristotle).
mortal_report:-write('Known mortals are:'),nl, mortal(X), write(X),nl,fail. 

把這個程式調入 Listener 中,執行 mortal_report. 。

?- mortal_report.
Known mortals are:
socrates
plato
aristotle
false.

以上程式中的一些函式以後還會詳細的介紹的。最後的那個 no 表示沒有其他的人了。
從下一章起,就開始正式介紹 Prolog 的程式設計方法了。我將用一個例項來介紹 Prolog ,這是一個文字的冒險遊戲,你所扮演的角色是一個三歲的小女孩,你想睡覺了,可是沒有毛毯( nani )你就不能安心的睡覺。所以你必須在那個大房子中找到你的毛毯,這就是你的任務。這個遊戲能夠顯示出一些 Prolog 的獨到之處,不過 Prolog 的功能遠不止編個簡單的遊戲,所以文中還將介紹一些其他的小程式。

事實 (facts)

注:斜粗體字表示 Prolog 的專有名詞

事實( facts )是 prolog 中最簡單的謂詞( predicate )。它和關聯式資料庫中的記錄十分相似。在下一章中我們會把事實作為資料庫來搜尋。
謂詞: Prolog 語言的基本組成元素,可以是一段程式、一個資料型別或者是一種關係。它由謂詞名和引數組成。兩個名稱相同而引數的數目不同的謂詞是不同的謂詞。

事實的語法結構如下:

pred(arg1, arg2, … argN).

其中 pred 為謂詞的名稱。 arg1 , … 為引數,共有 N 個。

‘ . ’是所有的 Prolog 子句的結束符。

沒有引數的謂詞形式如下:

pred.

引數可以是以下四種之一:

整數( integer)

絕對值小於某一個數的正數或負數。

原子( atom )

由小寫字母開頭的字串。

變數( variable )

由大寫字母或下劃線( _ )開頭。

結構( structure )

在以後的章節介紹。

不同的 Prolog 還增加了一些其他的資料型別,例如浮點數和字串等。

Prolog 字符集包括: 大寫字母, A-Z ;小寫字母, a-z ;數字, 0-9 ; ±/^,.~:.?#$ 等。

原子通常是字母和數字組成,開頭的字元必須是小寫字母。例如:

hello
twoWordsTogether
x14

為了方便閱讀,可以使用下劃線把單詞分開。例如:

a_long_atom_name
z_23

下面的是不合法的原子,

no-embedded-hyphens
123nodigitsatbeginning
Nocapsfirst

下劃線不能放在最前面
使用單引號擴起來的字符集都是合法的原子。例如:

‘this-hyphen-is-ok’
‘UpperCase’
‘embedded blanks’

下面的由符號組成的也是合法的原子:

o >,++

變數和原子相似, 但是開頭字元是大寫字母或是下劃線。例如:

X
Input_List

下劃線開頭的都是變數

_Z56

有了這些基本的知識,我們就可以開始編寫事實了。事實通常用來儲存程式所需的資料。

例如,某次商業買賣中的顧客資料。

customer/3 。( /3 表示 customer 有三個引數)
customer(‘John Jones’, boston , good_credit).
customer(‘Sally Smith’, chicago, good_credit).

必須使用單引號把顧客名引起來,因為它們是由大寫字母開頭的,並且中間有空格。

再看一個例子,視窗系統使用事實儲存不同的視窗資訊。在這個例子中引數有視窗名稱和視窗的位置座標。

window(main, 2, 2, 20, 72).
window(errors, 15, 40, 20, 78).

某個醫療專家系統可能有如下的疾病資料庫。

disease(plague, infectious).
{ 疾病(瘟疫,有傳染性)}

Prolog 的直譯器提供了動態儲存事實和規則的方法,並且也提供了訪問它們的方法。

資料庫的更新是通過執行‘ consult ’或‘ reconsult ’命令。我們也可以直接在直譯器中輸入謂詞,但是這些謂詞不會被儲存到硬碟上。

尋找 Nani

下面我們正式開始“尋找 Nani ”遊戲的編寫。我們從定義基本的事實開始,這些事實是本遊戲的基本的資料庫。它們包括:

房間和它們的聯絡
物體和它們的位置
物體的屬性
玩家在遊戲開始時的位置

“尋找 Nani ”遊戲的的房間格局

首先我們使用 room/1 謂詞定義房間,一共有五條子句,它們都是事實,

room(kitchen).
room(office).
room(hall).
room(‘dining room’).
room(cellar).

我們使用具有兩個引數的謂詞來定義物體的位置。第一個引數代表物體的名稱,第二個參數列示物體的位置。開始時,我們加入如下的物體。

location(desk, office).
location(apple, kitchen).
location(flashlight, desk).
location(‘washing machine’, cellar).
location(nani, ‘washing machine’).
location(broccoli, kitchen).
location(crackers, kitchen).
location(computer, office).

注意:我們定義的那些符號,例如: kitchen 、 desk 等對於我們是有意義的,可是它們對於 Prolog 是沒有任何意義的,完全可以使用任何符號來表示房間的名稱。

謂詞 location/2 的意思是“第一個引數所代表的物體位於第二個引數所代表的物體中”。

Prolog 能夠區別 location(sink, kitchen) 和 location(kitchen, sink) 。

因此,引數的順序是我們定義事實時需要考慮的一個重要問題。

下面我們來表達房間的聯絡。使用 door/2 來表示兩個房間有門相連,這裡遇到了一個小小的困難:

door(office, hall).

我們想要表達的意思是, office 和 hall 之間有一個門。可是由於 Prolog 能夠區分 door(office, hall) 和 door(hall, office) , 所以如果我們想要表達一種雙向的聯絡,就必須把每種聯絡都定義一遍。

door(office, hall).
door(hall, office).

引數的順序對定義物體的位置有幫助,可是在定義房間的聯絡時卻帶來了麻煩。我們不得不把每個房門都定義兩次!

在這一章裡,只定義單向的門,以後會很好地解決此問題的。

door(office, hall).
door(kitchen, office).
door(hall, ‘dining room’).
door(kitchen, cellar).
door(‘dining room’, kitchen).

下面定義某些物體的屬性,

edible(apple).
edible(crackers).
tastes_yucky(broccoli).

最後,定義手電筒(由於是晚上,玩家必須想找到手電筒,並開啟它才能到那些關了燈的房間)的狀態和玩家的初始位置。

turned_off(flashlight).
here(kitchen).

好了,到此你應該學會了如何使用 Prolog 的事實來表達資料了。

現在我們的遊戲中已經有了一些事實,使用 Prolog 的直譯器調入此程式後,我們就可以對這些事實進行查詢了。

本章和下一章中的 Prolog 程式只包括事實,我們要學會如何對這些事實進行查詢。

Prolog 的查詢工作是靠模式匹配完成的。查詢的模板叫做目標 (goal) 。如果有某個事實與目標匹配,那麼查詢就成功了, Prolog 的直譯器會回顯 ‘yes.’ 。如果沒有匹配的事實,查詢就失敗了,直譯器回顯 ‘no.’ 。

我們把 Prolog 的模式匹配工作叫做聯合 (unification) 。當資料庫中只包括事實時,以下三個條件是使聯合成功的必要條件。

目標謂詞名與資料庫中的某個謂詞名相同。
這兩個謂詞的引數數目相同。
所有的引數也相同。

在介紹查詢之前,讓我們回顧一下上一章所編寫的 Prolog 程式。

room(kitchen). 
room(office). 
room(hall). 
room('dining room'). 
room(cellar). 
door(office, hall). 
door(kitchen, office). 
door(hall, 'dining room'). 
door(kitchen, cellar). 
door('dining room', kitchen).
location(desk, office). 
location(apple, kitchen). 
location(flashlight, desk). 
location('washing machine', cellar). 
location(nani, 'washing machine'). 
location(broccoli, kitchen). 
location(crackers, kitchen). 
location(computer, office).
edible(apple). 
edible(crackers). 
tastes_yucky(broccoli). 
here(kitchen). 

以上是我們的“尋找 Nani ”中的所有事實。

把這段程式調入 Prolog 直譯器中後就可以開始進行查詢了。
我們的第一個問題是: office 在本遊戲中是不是一個房間。

?-room(office). {?- 是直譯器的提示符 }

yes.

Prolog 回答 yes ,因為它在資料庫中找到了 room(office). 這個事實。

我們繼續問:有沒有 attic 這個房間。

?-room(attic).

no.

Prolog 回答 no ,因為它在資料庫中找不到 room(attic). 這個事實。

同樣我們還可以進行如下的詢問。

?- location(apple, kitchen).

yes

?- location(kitchen, apple).

no

你看 Prolog 懂我們的意思呢,它知道蘋果在廚房裡,並且知道廚房不在蘋果裡。

但是下面的詢問就出問題了。

?- door(office, hall).

yes

?- door(hall, office).

no

由於我們定義的門是單方向的,結果遇到了麻煩。

在查詢目標中我們還可以使用 Prolog 的變數。這種變數和其他語言中的不同。叫它邏輯變數更合適一點。變數可以代替目標中的一些引數。

變數給聯合操作帶來了新的意義。以前聯合操作只有在謂詞名和引數都相同時才能成功。但是引入了變數之後,變數可以和任何的條目匹配。

當聯合成功之後,變數的值將和它所匹配的條目的值相同。這叫做變數的繫結 (binding) 。當帶變數的目標成功的和資料庫中的事實匹配之後, Prolog 將返回變數繫結的值。

由於變數可能和多個條目匹配, Prolog 允許你察看其他的繫結值。在每次 Prolog 的回答後輸入“;”,可以讓 Prolog 繼續查詢。下面的例子可以找到所有的房間。“;”是使用者輸入的。

?- room(X).
X = kitchen ;
X = office ;
X = hall ;
X = ‘dining room’ ;
X = cellar ;
no

最後的 no 表示找不到更多的答案了。

下面我們想看看 kitchen 中都有些什麼。(變數以大寫字母開始)

?- location(Thing, kitchen).
Thing = apple ;
Thing = broccoli ;
Thing = crackers ;
no

我們還可以使用兩個變數來查詢所有的物體及其位置。

?- location(Thing, Place).
Thing = desk
Place = office ;
Thing = apple
Place = kitchen ;
Thing = flashlight
Place = desk ;

no

查詢的工作原理

當 Prolog 試圖與某一個目標匹配時,例如: location/2 ,它就在資料庫中搜尋所有用 location/2 定義的子句,當找到一條與目標匹配時,它就為這條子句作上記號。當使用者需要更多的答案時,它就從那條作了記號的子句開始向下查詢。

我們來看一個例子,使用者詢問:

location (X,kitchen).

Prolog 找到資料庫中的第一條 location/2 子句,並與目標比較。

目標 location(X, kitchen)

子句 #1 location(desk, office) 匹配失敗,因為第二個引數不同,一個是 kitchen ,一個是 office 。於是 Prolog 繼續比較第二個子句。

目標 location(X, kitchen)

子句 #2 location(apple, kitchen) 這回匹配成功,而變數 X 的值就被繫結成了 apple 。

?- location(X, kitchen).
X = apple

如果使用者輸入分號 (? , Prolog 就開始尋找其他的答案。

首先它必須釋放( unbinds )變數 X 。然後從上一次成功的位置的下一條子句開始繼續搜尋。這個過程叫做回溯( backtracking )。在本例中就是第三條子句。

目標 location(X, kitchen)

子句 #3 location(flashlight, desk) 匹配失敗,直到第六條子句時匹配又成功了 。

目標 location(X, kitchen)

子句 #6 location(broccoli, kitchen) 結果變數 X 又被繫結為 broccoli ,直譯器顯示:

X = broccoli ;

再度輸入分號, X 又被解放,開始新的搜尋。又找到了:

X = crackers ;

這回再沒有新的子句能夠匹配了,於是 Prolog 回答 no ,表示最後一次搜尋失敗了。

no

要想了解 Prolog 的執行順序,最好的方法就是單步除錯程式,不過在此之前,還是讓我們加深一下對目標的認識吧。

Prolog 的目標有四個埠用來控制執行的流程:

呼叫( call )、退出( exit )、重試( redo )以及失敗( fail )。

一開始使用 Call 埠進入目標,如果匹配成功就到了 exit 埠,如果失敗就到了 fail 埠,如果使用者輸入分號,則又從 redo 埠進入目標。

下圖表示了目標和它的四個埠。

每個埠的功能如下:
call 開始使用目標搜尋子句。
exit 目標匹配成功,在成功的子句上作記號,並繫結變數。
redo 試圖重新滿足目標,首先釋放變數,並從上次的記號開始搜尋。
fail 表示再找不到更多的滿足目標的子句了。

下面列出了除錯 location(X,kitchen). 時的情況。括號中的數字表示當前正在考慮的子句。

?- location(X, kitchen).
CALL: - location(X, kitchen)
EXIT:(2) location(apple, kitchen)
X = apple;
REDO: location(X, kitchen)
EXIT:(6) location(broccoli, kitchen)
X = broccoli ;
REDO: location(X, kitchen)
EXIT:(7) location(crackers, kitchen)
X = crackers ;
FAIL - location(X, kitchen)
no

在 Prolog 的直譯器中輸入,

?- debug. (作者注:我用的版本為8.2.2,此命令不行,使用trace)

就可以開始除錯你的程式了。

回車/c:繼續

; :回溯。轉到當前目標的REDO埠;

f :失敗。轉到當前目標的FAIL埠;

r :重複。從EXIT埠轉至該目標的CALL埠;

s :跳。在目標的CALL埠告訴除錯程式路過所有的提示符,直到該目標的EXIT或FAIL埠;

L :顯示當前謂詞的全部子句;

a :中止。返回Prolog環境;

e :退出Prolog系統,它將放棄已做的全部工作;

h :幫助。顯示命令選項。

複雜查詢

我們可以把簡單的查詢連線起來,組成一些較複雜的查詢。例如,如果我們想知道廚房裡能吃的東西,就可以向 Prolog 進行如下的詢問。

?- location(X, kitchen), edible(X).

簡單的查詢只有一個目標,而混合查詢可以把這些目標連線起來,從而進行較為複雜的查詢。上面的連線符號 ‘,’ 是並且的意思。

上面的式子用語言來描述就是“尋找滿足條件的 X ,條件是: X 在廚房裡,並且 X 能吃。”如果某個變數在詢問中多次出現,則此變數在所有出現的位置都必須繫結為相同的值。所以上面的查詢只有找到某一個 X 的值,使得兩個目標都成立時,才算查詢成功。

每次查詢所使用的變數都是區域性的變數,它只在本查詢中有意義,所以當我們進行了如下的查詢後,

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

查詢結果中沒有 broccoli (椰菜),因為我們沒有把它定義為可吃的東西。此後,還可以用 X 進行其他的查詢。

?- room(X).
X = kitchen ;
X = office ;
X = hall ;
…;
no

除了使用邏輯的方法理解混合查詢外,還可以通過分析程式的執行步驟來理解。用程式的語言來說就是“首先找到一樣位於廚房的東西,然後判斷它能否食用,如果不能,就到廚房裡找下一樣東西,再判斷能否食用。一直如此重複,直到找到答案或把廚房的東西全部查完為止。”

下面我們再來看一個有兩個變數的例子。

?- door(kitchen, R), location(T,R).
R = office
T = desk ;
R = office
T = computer ;
R = cellar
T = ‘washing machine’ ;
no

上面的查詢用邏輯的語言來解釋就是:“找房間 R ,使得從廚房到房間 R 有門相連,並且把房間 R 中的物品 T( 這裡是房間 R 的所有物品)也找出來。”
下面是此查詢的單步執行過程。

Goal: door(kitchen, R), location(T,R)
1 CALL door(kitchen, R)
1 EXIT (2) door(kitchen, office)
2 CALL location(T, office)
2 EXIT (1) location(desk, office)
R = office
T = desk ;
2 REDO location(T, office)
2 EXIT (8) location(computer, office)
R = office
T = computer ;
2 REDO location(T, office)
2 FAIL location(T, office)
1 REDO door(kitchen, R)
1 EXIT (4) door(kitchen, cellar)
2 CALL location(T, cellar)
2 EXIT (4) location(‘washing machine’, cellar)
R = cellar
T = ‘washing machine’ ;
2 REDO location(T, cellar)
2 FAIL location(T, cellar)
1 REDO door(kitchen, R)
1 FAIL door(kitchen, R)
no

內部謂詞

講了這麼多了,我們還只是用到了 Prolog 的一些語法,完全沒有使用 Prolog 提供的一些內部的函式,我把這些內部函式稱為內部謂詞。

和其他的程式語言一樣, Prolog 也提供了一些基本的輸入輸出函式,下面我們要編寫一個較複雜的查詢,它能夠找到所有廚房裡能夠吃的東西,並把它們列出來。而不是像以前那樣需要人工輸入 ‘;’ 。

要想完成上面的任務,我們首先必須瞭解內部謂詞的概念。內部謂詞是指已經在 Prolog 中事先定義好的謂詞。在記憶體中的動態資料庫中是沒有內部謂詞的子句的。

當直譯器遇到了內部謂詞的目標,它就直接呼叫事先編好的程式。

內部謂詞一般所完成的工作都是與邏輯程式無關的,例如輸入輸出的謂詞。

所以我們可以把這些謂詞叫做非邏輯謂詞。但是這些謂詞也可以作為 Prolog 的目標,所以它們也必須擁有和邏輯謂詞相同的四個埠: Call 、 Fail 、 Redo 和 Exit 。

下面介紹幾個常用的輸出謂詞。

write/1

此謂詞被呼叫時永遠是成功的,並且它可以把它的引數作為字串輸出到螢幕上。當回溯時,它永遠是失敗,所以回溯是不會把已經寫到螢幕上的字元又給刪除的。

nl/0

此謂詞沒有引數,和 write 一樣,從 Call 埠呼叫時總是成功的,從 Redo 埠回溯時總是失敗的,它的作用是在螢幕上輸出一個回車符。

tab/1

此謂詞的引數是一個整數,它的作用是輸出 n 個空格, n 為它的引數。其控制流程與上面兩個相同。

下圖是一般情況下的 Prolog 目標的內部流程控制示意圖。我們將使用此圖和內部謂詞的流程控制圖相比較。

上圖中左上角的菱形方塊表示從 Call 埠進入目標時所進行的處理。它從某謂詞的第一個子句開始搜尋,如果匹配成功就到 Exit 埠,如果沒有找到任何一個子句與目標匹配就轉到 Fail 埠。
右下角的方塊表示從 Redo 埠進入目標時所進行的處理,從最近一次成功的子句開始向下搜尋,如果匹配成功就轉到 Exit 埠,如果沒有找個更多的子句滿足目標就轉到 Fail 埠。
I/O 謂詞的流程控制和上述的不同,它不會改變流程的方向,如果流程從它的左邊進入,就會從它的右邊流出;而如果從它的右邊進入,則會從它的左邊流出。請參考下圖理解。
I/O 謂詞不會改變變數的值,但是它們可以把變數的值輸出。
還有一個專門引起回溯的內部謂詞 fail/0 ,從它的名字不難看出,它的呼叫永遠是失敗的。如果 fail/0 從左邊得到控制權,則它立即把控制權再傳回到左邊。它不會從右邊得到控制,因為沒法通過 fail/0 把控制權傳到右側。它的內部流程控制如下:
以前我們是靠使用 ‘;’ 來進入目標的 Redo 埠的,並且變數的值的輸出是靠直譯器完成的。現在有了上面幾個內部謂詞,我們就可以靠 I/O 謂詞來顯示變數的值,靠 fail 謂詞來引起自動的回溯。
下面是此查詢語句及其執行結果。
?- location(X, kitchen), write(X) ,nl, fail.
apple
broccoli
crackers
no

下面是此查詢的流程圖。

下面是此查詢的單步除錯過程。

Goal: location(X, kitchen), write(X), nl, fail.
在這裡插入圖片描述
下面請你分析一下,

?- door(kitchen, R), write®, nl, location(T,R), tab(3), write(T), nl, fail.

的輸出的結果是什麼呢?

前面我們已經說過,謂詞是使用一系列的子句來定義的。以前我們所學習的子句是事實,現在讓我們來看看規則吧。

規則的實質就是儲存起來的查詢。它的語法如下:

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). 它代表玩家所在的位置。以後我們將學習如何改變此事實。現在來試是功能吧,
在這裡插入圖片描述

好了到此,我們已經學會了 Prolog 的基本程式設計方法,下一章將總結一下,並再舉幾個例子,此後我們將進入較深的學習。

總結

到現在為止,我們已經對 Prolog 有了一個基本的瞭解,現在有必要對我們所學過的知識做一個系統的總結。

Prolog 的程式是由一系列的事實和規則組成的資料庫。

規則之間的呼叫是通過聯合操作完成的, Prolog 能夠自動的完成模式匹配。

規則還可以呼叫內部謂詞,例如 write/1 。

我們可以在 Prolog 的直譯器中單獨地對規則進行查詢(呼叫)。

在 Prolog 的程式的執行流程方面我有了如下的認識:

規則的執行是通過 Prolog 內建的回溯功能實現的。

我們可以使用內部謂詞 fail 來強制實現回溯。

我們也可以通過加入一條引數為偽變數(下劃線)無 Body 部分的子句,來實現強制讓謂詞成功。

我們還學習了,

資料庫中的事實代替了一般語言中的資料結構。

回溯功能能夠完成一般語言中的迴圈操作。

而通過模式匹配能夠完成一般語言中的判斷操作。

規則能夠被單獨地除錯,它和一般語言中的模組相對應。

而規則之間的呼叫和一般語言中的函式的呼叫類似。

有了以上的知識,我們還可以編寫出一些讓其它語言的程式設計師吃驚的小程式。

栗子:

下面就舉一個分析家譜的程式。

假如我們把家族成員之間的父子關係和夫妻關係,以及成員的性別屬性定義為基本的事實資料庫,我們就可以編出許多規則來判斷其他的親戚關係了。

例如我們有如下的資料庫:

father(a,b).
father(a,d).
father(a,t).
father(b,c).
wife(aw,a).
wife(bw,b).
male(t).
female(d).
male©.

father(a,b). 表示 a 是 b 的父親。
wife(aw,a). 表示 aw 是 a 的妻子。
male(t). 表示 b 是男性。
female(d). 表示 d 是女性。

上面我們並沒有定義 a 、 b 、 aw 、 bw 的性別。 因為通過他們和其他人的關係我們可以很容易地確定他們的性別。不過要想讓 Prolog 知道他們的性別我們就要定義如下的規則。

male(X):-father(X, _).

female(X):-wife(X,_).

上面的 male/1 和 female/1 的謂詞名稱和事實的名稱相同,這並不是什麼特別的情況,你可以把所有定義相同的謂詞的子句之間的關係想象“或者”的關 系。也就是說: t 和 d 是男性,或者如果 X 是其他人的父親,則它也是男性。在判斷性別時,我們並不關心此人是誰的父親,所以後面一個變數用“ _ ”代替了。

好了,假如有如下的詢問:
?-male(t).
yes.
?-male(a).
yes.
?-male(X).
X=t;
X=c;
X=a;
X=a;
X=a;
X=b;
no.

最後一個詢問,它雖然把所有的男性找了出來,可是它把 a 找了三次,原因很簡單,因為我們有三個 father/2 的子句都包含 a ,好像不太理想,不過現在只能將就一下了,當我們學習了更多的知識後,就好解決了。

下面我們定義一些其他的親戚關係的規則。你大概一看就能夠理解。

例如: X 和 Y 是兄弟的條件是 :
X 和 Y 有相同的父親 {father(Z,X),father(Z,Y)} ,
並且他們都是男性 {male(X),male(Y)} ,
最後由於 X 和 Y 可以取相同的值,所以我們不得不加上一條 X 和 Y 不是同一個人 {X=Y} 。

grandfather(X,Y):-father(X,Z),father(Z,Y).
mother(X,Y):-wife(X,Z),father(Z,Y).
brother(X,Y):-father(Z,X),father(Z,Y),male(X),male(Y),X=Y.

當然我們還可以加入更復雜一點的規則,

uncle(X,Y):-brother(X,Z),father(Z,Y).

這個叔伯的規則 uncle/2 呼叫了前面的規則 brother/2 。

這裡只是簡單回顧一下前面所學習的知識,所以這個家族程式雖然可以使用,但是卻極不完善。

例如:它會把某一答案重複多次,還不能描述沒有小孩的丈夫的性別。

我們這樣改一下會更好一點: male(X):-wife(_,X) 。

因此,規則的定義是多種多樣的,到底哪種更好、哪種更快,這就是我們以後所要研究的問 題之一了。

數學計算

Prolog 中也有一些能夠進行數學計算的功能,但是數學計算是不好用邏輯的事物來描述的。因此計算一個數學表示式的方法和我們以前所學習的模式匹配有很大的區別。因此, Prolog 專門提供了內部謂詞 is 來計算數學表示式。
其語法形式如下:

X is < 數學表示式 >

變數 X 將被賦值為表示式的值,在回溯時不賦值。數學表示式的形式和其他的語言相同。

下面是使用 Prolog 計算的一些例子。

?- X is 2 + 2.
X = 4
?- X is 3 * 4 + 2.
X = 14

我們還可以使用括號,

?- X is 3 * (4 + 2).
X = 18
?- X is (8 / 4) / 2.
X = 1

除了 is 以外, Prolog 還提供了一些用來比較大小的操作符。

X > Y
X < Y
X >= Y
X =< Y

請注意 >= 和 =< ,它們的符號順序是不能顛倒的。下面是一些例子,

?- 4 > 3.
yes
?- 4 < 3.
no
?- X is 2 + 2, X > 3.
X = 4
?- X is 2 + 2, 3 >= X.
no
?- 3+4 > 3*2.
yes

我們可以在規則中使用這些符號,例如,

c_to_f(C,F) :- F is C * 9 / 5 + 32.
freezing(F) :- F =< 32.

c_to_f/2 把攝氏溫度轉換為華氏溫度, freezing 判斷華氏溫度的冰點。

下面是使用這些謂詞的例子。

?- c_to_f(100,X).
X = 212
yes
?- freezing(15).
yes
?- freezing(45).
no

動態修改資料庫

Prolog 的程式就是謂詞的資料庫,我們通常把這些謂詞的子句寫入 Prolog 的程式中的。在執行 Prolog 時,直譯器首先把所有的子句調入到記憶體中。所以這些寫在程式中的子句都是固定不變的。那麼有沒有辦法動態地控制記憶體中的子句呢?

Prolog 提供了這方面的功能。這就意味著, Prolog 程式在執行過程中,還能夠改變它自己。

它使用一些內部謂詞來完成這個功能。最重要的幾個謂詞如下:

asserta(X)

把子句 X 當作此子句的謂詞的第一個子句加入到動態資料庫中。它和 I/O 內部謂詞的流程控制相同。回溯是失敗,並且不會取消它所完成的工作。

例如:如果記憶體中已經有了下面的幾個事實:

people(a).
people(b).
people©.

如果執行了 asserta(people(d)) 之後,記憶體中的 people/1 的子句就變成了下面這個樣子:

people(d).
people(a).
people(b).
people©.
asserta(X)

和 asserta/1 的功能類似,只不過它把 X 子句追加為最後一個子句。

retract(X)

把子句 X 從動態資料庫中刪除。此操作也是永久性的,也就是說回溯的時候不能撤銷此操作。

在 swi prolog 中需要對動態操作的謂詞名進行宣告,例如前面如果希望能夠動態修改 people/1 的子句,需要在程式最前面執行:

:-dynamic people/1.

能夠動態的修改資料庫顯然是很重要的。它有助於我們完成“尋找 Nani ”。使用這些謂詞,我們可以很方便地改變玩家和物體的位置。

下面我們來設計 goto/1 這個謂詞,它能夠把玩家從一個房間移到另一個房間。

我們採取從頂向下的設計方法,這和我們設計 look/0 時的方法不同。

當玩家鍵入了 goto 命令之後,首先判斷他能否去他想去的位置,如果可以,則移動到此位置,並把此位置的情況告訴玩家。

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

下面來一步一步地完成這些還沒定義的謂詞。

玩家所能夠去的房間的條件是:此房間和玩家所在的房間是相通的,即:

can_go(Place):- here(X), connect(X, Place).

我們可以馬上測試一下,(假定玩家在廚房)

?- can_go(office).
yes
?- can_go(hall).
no

現在 can_go/1 已經可以工作了,但是如果它在失敗時能夠給出一條訊息就很好了。所以還需要另外增加一條子句,如果第一條子句失敗,也就是說不能去那個房間時,第二個子句將顯示一條訊息。

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

注意第二條子句最後的那個 fail ,因為當目標與第二條子句匹配時,表示不能去此房間,所以它應該返回 fail 。這次的執行結果比上次要好多了。

?- can_go(hall).
You can’t get there from here.
no

下面再來設計 move/1 謂詞,它必須能夠動態的修改資料庫中的 here 謂詞的子句。首先把玩家的舊位置的資料刪除,再加上新位置的資料。

move(Place):- retract(here(X)), asserta(here(Place)).

現在我們可以使用 goto/1 在遊戲的所有房間裡走動了。

?- goto(office).
You are in the office
You can see:
desk
computer
You can go to:
hall
kitchen
yes
?- goto(hall).
You are in the hall
You can see:
You can go to:
dining
room
office
yes
?- goto(kitchen).
You can’t get there from here.
no

好像有點遊戲的味道了。 ?

下面開始編寫 take 和 put 謂詞,使用這兩個謂詞,我們可以拿取或丟棄遊戲中的物品。

使用 have/1 謂詞來儲存玩加身上所攜帶的物品,一開始,玩家身上沒有物品,所以我們沒有在程式的事實中定義 have/1 謂詞。

take(X):- can_take(X), take_object(X).

其中 can_take(X) 的設計方法與 can_go/1 相同。

can_take(Thing) :- here(Place), location(Thing, Place).
can_take(Thing) :-write(‘There is no ‘), write(Thing), write(’ here.’), nl, fail.

take_object/1 與 move/1 類似,

它首先刪除一條 location/1 的子句,然後新增一條 have/1 的子句。這反映出了物品從其所在位置移到玩家身上的過程。

take_object(X):-retract(location(X,_)), asserta(have(X)), write(‘taken’), nl.

正如我們所看到的那樣, Prolog 子句中的變數全部都是區域性變數。

與其他的語言不同,在 Prolog 中沒有全域性變數,取而代之的是 Prolog 的資料庫。它使得所有的 Prolog 子句能夠共享資訊。而 asserta 和 retract 就是控制這些全域性資料的工具。

使用全域性資料有助於在子句之間快速的傳遞資訊。不過,這種方式隱藏了子句之間的呼叫關係,所以一旦程式出錯,是很難找到原因的。

我們完全也可以不使用 assert 和 retract 來完成上述的功能,不過這就需要把資訊作為引數在子句中傳遞。

在這種情況下,遊戲中的狀態將使用謂詞的引數來儲存,而不是謂詞的子句。

每一個謂詞的入口引數是當前狀態,而出口引數則為此謂詞修改後的狀態,狀態在謂詞之間傳遞,從而達到了預期的目的。我們還將在以後的章節中介紹這種方法。

我們現在所編寫的程式並不都是從純邏輯的考慮出發的,不過你可以看出使用 Prolog 編寫這個遊戲的過程非常自然,並沒有什麼晦澀難懂的東西。

一般情況下, asserta 等謂詞是不會在回溯的時候還原資料庫的,所以上面的幾個資料管理謂詞的內部流程與 I/O 謂詞相同,不過我們可以很容易的編寫出能夠在回溯時取消修改的謂詞。

backtracking_assert(X):- asserta(X).
backtracking_assert(X):- retract(X),fail.

首先第一個子句被執行,在資料庫中新增一條 X 子句。當其後的目標失敗而產生回溯時,第二個子句將被呼叫,於是它把第一個子句的操作給取消了,又把子句 X 從資料庫中上除了。

到目前為止,所介紹的事實、查詢以及規則都使用的是最簡單的資料結構。謂詞的引數都是原子或者整數,這些都是 Prolog 的基本組成元素。

例如我們所使用過的原子有 :

office, apple flashlight, nani

通過把這些最簡單的資料組合起來,可以生成複雜的資料型別,我們稱之為結構

結構由結構名和一定數量的引數組成。這與以前所學過的目標和事實是一樣的。

functor(arg1,arg2,…)

結構的引數可以是簡單的資料型別或者是另一個結構。

現在在遊戲中的物品都是由原子表示的,例如, desk 、 apple 。

但是使用結構可以更好的表達這些東西。

下面的結構描述了物品的顏色、大小以及重量。

object(candle, red, small, 1).
object(apple, red, small, 1).
object(apple, green, small, 1).
object(table, blue, big, 50).

這些結構可以直接取代原來的 location/2 中的引數。但是這裡我們再定義一個謂詞 location_s/2 。注意,雖然定義的結構較為複雜,但是它仍然是 location_s/2 的一個引數。

location_s(object(candle, red, small, 1), kitchen).
location_s(object(apple, red, small, 1), kitchen).
location_s(object(apple, green, small, 1), kitchen).
location_s(object(table, blue, big, 50), kitchen).

Prolog 的變數是沒有資料型別之分的,所以它可以很容易的繫結為結構,如同它繫結為原子一樣。事實上,原子就是沒有引數的最簡單的結構。因此可以有如下的詢問。

?- location_s(X, kitchen).
X = object(candle, red, small, 1) ;
X = object(apple, red, small, 1) ;
X = object(apple, green, small, 1) ;
X = object(table, blue, big, 50) ;
no

我們還可以讓變數繫結為結構中的某些引數,下面的詢問可以找出廚房中所有紅色的東西。

?- location_s(object(X, red, S, W), kitchen).
X = candle
S = small
W = 1 ;
X = apple
S = small
W = 1 ;
no

如果不關心大小和重量,可以使用下面的詢問,其中變數‘ _ ’是匿名變數。

?- location_s(object(X, red, _, _), kitchen).
X = candle ;
X = apple ;
no

使用這些結構,可以使得遊戲更加真實。例如,我們可以修改以前所編寫的 can_take/1 謂詞,使得只有較輕的物品才能被玩家攜帶。

can_take_s(Thing) :-here(Room), location_s(object(Thing, , small,), Room).

同時,也可以把不能拿取某物品的原因說得更詳細一些,現在有兩個拿不了物品的原因。為了讓 Prolog 在回溯時不把兩個原因同時顯示出來,我們為每個原因建立一條子句。這裡要用到內部謂詞 not/1 ,它的引數是一個目標,如果此目標失敗,則它成功;目標成功則它失敗。例如,

?- not( room(office) ).
no
?- not( location(cabbage, ‘living room’) )
yes

注意,在 Prolog 中的 not 的意思是:不能通過當前資料庫中的事實和規則推出查詢的目標。下面是使用 not 重新編寫的 can_take_s/1 。

can_take_s(Thing) :- here(Room),location_s(object(Thing, _, small, _), Room).
can_take_s(Thing) :-here(Room),location_s(object(Thing, _, big, _), Room),write(‘The ‘), write(Thing), write(’ is too big to carry.’), nl,fail.
can_take_s(Thing) :-here(Room),not (location_s(object(Thing, _, _, _), Room)),write(‘There is no ‘), write(Thing), write(’ here.’), nl,fail.

下面來試試功能,假設玩家在廚房裡。

?- can_take_s(candle).
yes
?- can_take_s(table).
The table is too big to carry.
no
?- can_take_s(desk).
There is no desk here.
no

原來的 list_things/1 謂詞也可以加上一些功能,下面的 list_things_s/1 不但可以列出房間中的物品,還可以給出它們的描述。

list_things_s(Place) :-
location_s(object(Thing, Color, Size, Weight),Place),
write(‘A ‘),write(Size),tab(1),
write(Color),tab(1),
write(Thing), write(’, weighing ‘),
write(Weight), write(’ pounds’), nl,
fail.
list_things_s(_)

它的回答令人滿意多了。

?- list_things_s(kitchen).
A small red candle, weighing 1 pounds
A small red apple, weighing 1 pounds
A small green apple, weighing 1 pounds
A big blue table, weighing 50 pounds
yes

如果你覺得使用 1 pounds 不太準確的話,我們可以再使用另一個謂詞來解決此問題。

write_weight(1) :- write(‘1 pound’).
write_weight(W) :- W > 1, write(W), write(’ pounds’).

下面試試看
?- write_weight(4).
4 pounds
yes
?- write_weight(1).
1 pound
yes

第一個子句中不需要使用 W=1 這樣的判斷,我們可以直接把1寫到謂詞的引數中,因為只有為1時是使用單數,其他情況下都使用複數。
第二個子句中需要加入 W>1 ,要不然當重量為1時兩條子句就同時滿足。

結構可以任意的巢狀,下面使用 dimension 結構來描述物體的長、寬、高。

object(desk, brown, dimension(6,3,3), 90).

當然,也可以這樣來表達物品的特性

object(desk, color(brown), size(large), weight(90))

下面是針對它的一條查詢。

location_s(object(X, _, size(large), _), office).

要注意變數的位置喲,不要搞混了。

聯合 (Unification)

Prolog 的最強大的功能之一就是它內建了模式匹配的演算法 ---- 聯合 (Unification) 。

以前我們所介紹的例子中的聯合都是較為簡單的。現在來仔細研究一下聯合。

下表中列出了聯合操作的簡要情況。

變數 & 任何專案 : 變數可以與任何專案繫結,其中也包括變數

原始專案 & 原始專案 : 兩個原始專案(原子或整數)只有當它們相同時才能聯合。

結構 & 結構 : 如果兩個結構的每個相應的引數能聯合,那麼這兩個結構可以聯合。

為了更清楚地介紹聯合操作,我們將使用 Prolog 的內部謂詞‘ =/2 ’,此謂詞當它的兩個引數能夠聯合時成功,反之則失敗。它的語法如下:

=(arg1, arg2)
為了方便閱讀,也可以寫成如下形式:

arg1 = arg2

注意:此處的等號在 Prolog 中的意義與其他語言中的不同。它不是數學運算子或者賦值符。

使用 = 進行聯合操作與 Prolog 使用目標與子句聯合時相同。在回溯時,變數將被釋放。

下面舉了幾個最簡單的聯合的例子。

?- a = a.
yes
?- a = b.
no
?- location(apple, kitchen) = location(apple, kitchen).
yes
?- location(apple, kitchen) = location(pear, kitchen).
no
?- a(b,c(d,e(f,g))) = a(b,c(d,e(f,g))).
yes
?- a(b,c(d,e(f,g))) = a(b,c(d,e(g,f))).
no

在下面的例子中使用的變數,注意變數是如何繫結為某個值的。
?- X = a.
X = a
?- 4 = Y.
Y = 4
?- location(apple, kitchen) = location(apple, X).
X = kitchen

當然也可以同時使用多個變數。

?- location(X,Y) = location(apple, kitchen).
X = apple
Y = kitchen
?- location(apple, X) = location(Y, kitchen).
X = kitchen
Y = apple

變數之間也可以聯合。每個變數都對應一個 Prolog 的內部值。當兩個變數之間進行聯合時, Prolog 就把它們標記為相同的值。在下面的例子中,我們假設 Prolog 使用‘ _nn ’,其中‘ n ’為數字,代表沒有繫結的變數。

?- X = Y.
X = _01
Y = _01
?- location(X, kitchen) = location(Y, kitchen).
X = _01
Y = _01

Prolog 記住了被繫結在一起的變數,這將在後面的繫結中反映出來,請看下面的例子。

?- X = Y, Y = hello.
X = hello
Y = hello
?- X = Y, a(Z) = a(Y), X = hello.
X = hello
Y = hello
Z = hello

最後的這個例子能夠很好地說明 Prolog 的變數繫結與其他語言中的變數賦值的區別。
請仔細分析下面的詢問。

?- X = Y, Y = 3, write(X).
3
X = 3
Y = 3
?- X = Y, tastes_yucky(X), write(Y).
broccoli
X = broccoli
Y = broccoli

當兩個含變數的結構之間進行聯合時,變數所取的值使得這兩個結構相同。

?- X = a(b,c).
X = a(b,c)
?- a(b,X) = a(b,c(d,e)).
X = c(d,e)
?- a(b,X) = a(b,c(Y,e)).
X = c(_01,e)
Y = _01

無論多麼複雜, Prolog 都將準確地記錄下變數之間的關係,一旦某個變數繫結為某值,與之有關的變數都將改變。

?- a(b,X) = a(b,c(Y,e)), Y = hello.
X = c(hello, e)
Y = hello
?- food(X,Y) = Z, write(Z), nl, tastes_yucky(X), edible(Y), write(Z). food(_01,_02)
food(broccoli, apple)
X = broccoli
Y = apple
Z = food(broccoli, apple)

如果在兩次繫結中變數的值發生衝突,那麼目標就失敗了。
?- a(b,X) = a(b,c(Y,e)), X = hello.
no

上面的例子中,第二個子目標失敗了,因為找不到一個 y 的值使得 hello 與 c(Y,e) 之間能夠聯合。而下面的例子是成功的。

?- a(b,X) = a(b,c(Y,e)), X = c(hello, e).
X = c(hello, e)
Y = hello

如果變數不能繫結為某一可能的值,那麼聯合也將失敗。

?- a(X) = a(b,c).
no
?- a(b,c,d) = a(X,X,d).
no

下面的這個例子很有趣,請你研究一下吧。
?- a(c,X,X) = a(Y,Y,b).
no

你明白為什麼這個例子失敗麼?第一個引數的繫結使得 Y 繫結為 c ,第二個引數之間的繫結告訴 Prolog 變數 X 與 Y 的值相同,那麼 X 也繫結 c ,而最後一個引數的繫結使得 X 為 b ,有矛盾,所以失敗了。
這就是說沒有什麼辦法能使得這兩個結構聯合。

匿名變數( _ )不會繫結為任何值。所以也不要求它所出現的位置的值必須相同。

?- a(c,X,X) = a(,,b).
X = b

如果使用( = )那麼聯合操作是顯式的。而 Prolog 在使用子句與目標匹配時的聯合則是隱式的。

列表

為了能夠更好地表達一組資料, 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)]

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

操作符語法

我們已經學習過了 Prolog 的資料結構 , 它的形式如下:
functor(arg1,arg2,…,argN).

這是 Prolog 的唯一的資料結構,但是 Prolog 允許這種資料結構有其它的表達方法(僅僅是表達方法不同)。

這種表達方法有時候更加接近我們的習慣,正如列表的兩種表達法一樣。

現在要介紹的是操作符語法

以前曾經介紹了數學符號,在這一章我們將看到它和 Prolog 的資料結構是等價的,並且學習如何定義自己的操作符。
所有的數學操作符都是 Prolog 的基本符號,例如 -/2 、 +/2 、 -/1 。

使用謂詞 display/1 可以看到它們的標準的語法結構。

?- display(2 + 2).
+(2,2)
?- display(3 4 + 6).
+((3,4),6)
?- display(3 (4 + 6)).
(3,+(4,6))

你可以把任何謂詞定義為操作符的形式,例如,如果我們把 location/2 定義為了操作符,那麼我們就可以用:

apple location kitchen.
來代替
location(apple, kitchen).

注意:這只是書寫形式上的不同,在 Prolog 進行模式匹配時它們都是一樣的。

操作符有三種形式:
中綴( infix ):例如 3+4
字首( prefix ):例如 -7
字尾( postfix ):例如 8 factorial

每個操作符有不同的優先權值,從 1 到 1200 。當某句中有多個操作符時,優先權高的將先被考慮。優先權值越小優先權越高。

使用內部謂詞 op/3 來定義操作符,它的三個引數分別是:優先權、結合性、操作符名稱。

結合性使用模板來定義,例如**中綴操作符使用“ xfx ”**來定義。“ f ”表示操作符的位置。
下面我們將重新編寫 location/2 謂詞,並改名為is_in/2 。

is_in(apple, room(kitchen)).

使用 op/3 謂詞把 is_in/2 定義為操作符,優先權值為 35 。

?- op(35,xfx,is_in).

下面是我們的詢問。

?- apple is_in X.
X = room(kitchen)
?- X is_in room(kitchen).
X = apple

同樣可以使用操作符來定義事實。
banana is_in room(kitchen).
為了證明這兩種資料結構是等價,我們可以進行如下的比較:
?- is_in(banana, room(kitchen)) = banana is_in room(kitchen).
yes

使用 display/1 可以清楚地看到這一點。
?- display(banana is_in room(kitchen)).
is_in(banana, room(kitchen))

下面再把 room/1 定義為字首操作符。字首操作符的模板是 fx 。它的優先權應該比 is_in 的高。這裡取 33 。

?- op(33,fx,room).
?- room kitchen = room(kitchen).
yes
?- apple is_in X.
X = room kitchen

使用上面的兩個操作符,我們可以使用如下的方式定義事實。

pear is_in room kitchen.
?- is_in(pear, room(kitchen)) = pear is_in room kitchen.
yes
?- display(pear is_in room kitchen).
is_in(pear, room(kitchen))

注意如果操作符的優先權搞錯了,那就全部亂了套。例如:如果 room/1 的優先權低於 is_in/2 ,那麼上面的結構就變成了下面這個樣子:
room(is_in(apple, kitchen))

不但如此, Prolog 的聯合操作也將出現問題。所以一定要仔細考慮操作符的優先權。
最後我們來定義字尾操作符,使用模板 xf

?- op(33,xf,turned_on).
flashlight turned_on.
?- turned_on(flashlight) = flashlight turned_on.
yes

使用操作符可以是程式更容易閱讀。

在我們的命令驅動的“尋找 Nani ”遊戲中,為了使發出的命令更接近自然語言,可以使用操作符來定義。

goto(kitchen) -> goto kitchen.
turn_on(flashlight) -> turn_on flashlight.
take(apple) -> take apple.

雖然這還不是真正的自然語言,可是比起帶括號的來還是方便多了。
當操作符的優先權相同時, Prolog 必須決定是從左到右還是從右到左地讀入操作符。這就是操作符的左右結合性。有些操作符沒有結合性,如果你把兩個這種操作符放到一起將產生錯誤。
下面是結合性的模板:

Infix:
xfx non-associative (沒有結合性)
xfy right to left
yfx left to right

Prefix
fx non-associative
fy left to right

Postfix:
xf non-associative
yf right to left

前面所定義的謂詞 is_in/2 沒有結合性,所以下面的句子是錯誤的。
key is_in desk is_in office.
為了表示這種巢狀關係,我們可以使用從右到左的結合性。
?- op(35,xfy,is_in).
yes
?- display(key is_in desk is_in office).
is_in(key, is_in(desk, office))
如果使用從左到右的結合性,我們的結果將不同。
?- op(35,yfx,is_in).
yes
?- display(key is_in desk is_in office).
is_in(is_in(key, desk), office)

但是使用括號可以改變這種結合性:
?- display(key is_in (desk is_in office)).
is_in(key, is_in(desk, office))

由許多內部謂詞都定義為了中綴操作符。因此我們可以使用“ arg1 predicate arg2. ”來代替 predicate(arg1,arg2) 。
我們所見過的數學符號就是如此,例如 ±/ 。但是一定要牢記這只是表達形式上的區別,因此 3+4 和 7 是不一樣的,它就是 + ( 3 , 4 )。
只有一些特殊的內部謂詞(例如 is/2 )進行真正的數學運算。 is/2 計算它右邊表示式的值,並讓左邊繫結為此值。它與聯合( = )謂詞是不同的, = 只進行聯合而不進行計算。
?- X is 3 + 4.
X = 7
?- X = 3 + 4.
X = 3 + 4
?- 10 is 5 2. (8.2.2 版本,乘需要用*
yes
?- 10 = 5 2.
no
?- X is 3 4 + (6 / 2).
X = 15
?- X = 3 4 + (6 / 2).
X = 3 4 + (6 / 2)
?- X is +((3,4) , /(6,2)).
X = 15
?- 3*4 + (6 / 2) = +(: create(3,4),/(6,2)).
yes

只有當使用 is/2 來計算時,數學操作符才顯示出其不同之處,而一般情況下與其它的謂詞沒有任何區別。
?- X = 3 4 + likes(john, 6/2).
X = 3 4 + likes(john, 6/2).
?- X is 3 4 + likes(john, 6/2).
error

我們已經知道 Prolog 的程式是由一系列的子句構成的。其實這些子句也是使用操作符書寫的 Prolog 的資料結構。這裡的操作符是 “:-” ,它是中綴操作符,有兩個引數。

:-(Head, Body).
Body 也是由操作符書寫的資料結構。這裡的操作符為 “,” ,它表示並且的意思,所以 Body 的形式如下:

,(goal1, ,(goal2,goal3))

好像看不明白,操作符 “,” 與分隔符 “,” 無法區別,所以我們就是用 “&” 來代替操作符 “,” ,於是上面的形式就變成了下面這個樣子了。

&(goal1, &(goal2, & goal3))

下面的兩種形式表達的意思是相同的。

head :- goal1 & goal2 & goal3.
:-(head, &(goal1, &(goal2, & goal3))).
實際上是下面的形式:
head :- goal1 , goal2 , goal3.
:-(head, ,(goal1, ,(goal2, , goal3))).

數學操作符不但可以用來計算,還有許多其它的用途。例如 write/1 ,只能有一個引數,當我們想同時顯示兩個變數的值時, 就可以使用下面的方法。
?- X = one, Y = two, write(X-Y).
one - two

因為 X-Y 實際上是一個資料結構,所以它相對於 write 來說就只是一個引數。

當然其它的數學操作符也能完成相同的功能,例如 / 。在有些 Prolog 的版本中乾脆引入了“:”這個操作符來專門完成這種任務,有了它我們可以很方便的書寫複雜的資料結構了。

object(apple, size:small, color:red, weight:1).
?- object(X, size:small, color:C, weight:W).
X = apple
C = red
W = 1
這裡我們使用 size:small ,代替了原來的 size(small) ,實際上**“:”是中綴操作符**,它的原始表達形式是 :(size,small) 。
從 這一章所介紹的內容我們可以發現 Prolog 的程式實際上也是一種資料結構,只不過是使用專門的操作符連線起來的。那麼到現在為止,我們所學習過的所有 Prolog 內容:事實、規則、結構、列表等的實質都是一樣的,這也正是 Prolog 與其它語言的最大區別 — 程式與資料的高度統一。正是它的這種極其簡潔的表達形式,使得它被廣泛地應用於人工智慧領域。

謂詞cut

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

但是,並不是所有的回溯都是必須的,這時我們需要能夠人工地控制回溯過程。

Prolog 提供了完成此功能的謂詞,**它叫做 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…

流程控制

在前面的章節中,我們瞭解了 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 特別適合開發自然語言的應用系統。在這一章,我們將為尋找 Nani 遊戲新增自然語言理解的部分。(由於 Prolog 謂詞是使用的英文符號,所以這裡的自然語言理解只能侷限在英文中)
在著手於編制尋找 Nani 之前, 我們先來開發一個能夠分析簡單英語句子的模組。把這種方法掌握之後,編制尋找 Nani 的自然語言部分就不在話下了。
下面是兩個簡單的英語句子:
The dog ate the bone.
The big brown mouse chases a lazy cat.
我們可以使用下面的語法規則來描述這種句子。
sentence : ( 句子)
nounphrase, verbphrase.
nounphrase : (名詞短語)
determiner, nounexpression.
nounphrase : (名詞短語)
nounexpression.
nounexpression :
noun.
nounexpression :
adjective (形容詞) , nounexpression.
verbphrase : (動詞短語)
verb, nounphrase.
determiner : (限定詞)
the | a.
noun : (名詞)
dog | bone | mouse | cat.
verb : (動詞)
ate | chases.
adjective :
big | brown | lazy.

稍微解釋一下:第一條規則說明一個句子有一個名詞短語和一個動詞短語構成。最後的一個規則定義了單詞 big 、 brown 和 lazy 是形容詞,中間的“ | ”表示或者的意思。

首先,來判斷某個句子是否是合法的句子。我們編寫了 sentence/1 謂詞,它可以判斷它的引數是否是一個句子。

句子必須用 Prolog 的一種資料結構來表達,這裡使用列表。

例如,前面的兩個句子的 Prolog 表達形式如下:
[the,dog,ate,the,bone]
[the,big,brown,mouse,chases,a,lazy,cat]

分析句子的方法有兩種。第一種是選擇並校樣的方法(見後面的人工智慧例項部分),使用這種方法,首先把句子的可能分解情況找出來,再來測試被分解的每一個部 分是否合法。我們前面已經介紹過使用 append/3 謂詞能夠把列表分成兩個部分。使用這種方法,頂層的規則可以是如下的形式:
sentence(L) :-
append(NP, VP, L),
nounphrase(NP),
verbphrase(VP).
append/3 謂詞可以把列表 L 的所有可能的分解情況窮舉出來,分解後的兩個部分為 NP 和 VP ,其後的兩個目標則分別測試 NP 和 VP 是否是合法的,如果不是則會產生回溯,從而測試其他的分解情況。
謂詞 nounphrase/1 和 verbphrase/1 的編寫方法與 sentence/1 基本相同,它們呼叫其他的謂詞來判斷句子中的更小的部分是否合法,只到呼叫到定義單詞的謂詞,例如:
verb([ate]).
verb([chases]).
noun([mouse]).
noun([dog]).
差異表
前面的這種方法效率是非常低的,這是因為選擇並校驗的方法需要窮舉所有的情況,更何況在每一層的目標之中都要進行這種測試。
更有效的方法就是跳過選擇的步驟,而直接把整個列表傳到下一級的謂詞中,每個謂詞把自己所要尋找的語法元素找出來,並返回剩下的列表。
為了能夠達到這個目標,我們需要介紹一種新的資料結構:差異表。它由兩個相關的表構成,第一個表稱為全表,而第二個表稱為餘表。這兩個表可以作為謂詞的兩個引數,不過我們通常使用‘ - ’連線這兩個表,這樣易於閱讀。它的形式是 X-Y 。
我們使用差異表改寫了第一條語法規則。如果能夠從列表 S 的頭開始,提取出一個名詞短語,其餘部分 S1, 並且能夠從 S1 的頭開始,提取出一個動詞短語,並且其餘部分為空表,那麼列表 S 是一個句子。(這句話要細心理解,差異表所表示的表是全表和餘表之間的差異。)
sentence(S) :-
nounphrase(S-S1),
verbphrase(S1-[]).
我們先跳過謂詞 nounphrase/1 和 verbphrase/1 的編寫,而來看看是如何定義真正的單詞的。這些單詞也必須書寫成差異表的形式,這個很容易做到:如果列表的第一個元素是所需的單詞,那麼餘表就是除去第一個單詞的表。
noun([dog|X]-X).
noun([cat|X]-X).
noun([mouse|X]-X).
verb([ate|X]-X).
verb([chases|X]-X).
adjective([big|X]-X).
adjective([brown|X]-X).
adjective([lazy|X]-X).
determiner([the|X]-X).
determiner([a|X]-X).
下面是兩個簡單的測試,
?- noun([dog,ate,the,bone]-X).
% 第一個單詞 dog 是名詞,於是成功,並且餘表是後面的元素組成的表。
X = [ate,the,bone]
?- verb([dog,ate,the,bone]-X).
no
我們把剩下的一些語法規則寫完:
nounphrase(NP-X):-
determiner(NP-S1),
nounexpression(S1-X).
nounphrase(NP-X):-
nounexpression(NP-X).
nounexpression(NE-X):-
noun(NE-X).
nounexpression(NE-X):-
adjective(NE-S1),
nounexpression(S1-X).
verbphrase(VP-X):-
verb(VP-S1),
nounphrase(S1-X).
注意謂詞 nounexpression/1 的遞迴定義,這樣就可以處理名詞前面有任意多個形容詞的情況。
我們來用幾個句子測試一下:
?- sentence([the,lazy,mouse,ate,a,dog]).
yes
?- sentence([the,dog,ate]).
no
?- sentence([a,big,brown,cat,chases,a,lazy,brown,dog]).
yes
?- sentence([the,cat,jumps,the,mouse]).
no
下面是單步跟蹤某個句子的情況:
詢問是
?- sentence([dog,chases,cat]).
1-1 CALL sentence([dog,chases,cat])
2-1 CALL nounphrase([dog,chases,cat]-_0)
3-1 CALL determiner([dog,chases,cat]-_0)
3-1 FAIL determiner([dog,chases,cat]-_0)
2-1 REDO nounphrase([dog,chases,cat]-_0)
3-1 CALL nounexpression([dog,chases,cat]- _0)
4-1 CALL noun([dog,chases,cat]-_0)
4-1 EXIT noun([dog,chases,cat]-
[chases,cat])

注意,表示餘表的變數的繫結操作是直到延伸至最底層時才進行的,每一層把它的餘表和上一層的繫結。這樣,當到達了詞彙層時,繫結的值將通過巢狀的呼叫返回。
3-1 EXIT nounexpression([dog,chases,cat]-
[chases,cat])
2-1 EXIT nounphrase([dog,chases,cat]-
[chases,cat])
現在已經找出了名詞短語,下面來測試餘表是否為動詞短語。
2-2 CALL verbphrase([chases,cat]-[])
3-1 CALL verb([chases,cat]-_4)
3-1 EXIT verb([chases,cat]-[cat])
很容易地就找出了動詞,下面尋找最後的動詞短語。
3-2 CALL nounphrase([cat]-[])
4-1 CALL determiner([cat]-[])
4-1 FAIL determiner([cat]-[])
3-2 REDO nounphrase([cat]-[])
4-1 CALL nounexpression([cat]-[])
5-1 CALL noun([cat]-[])
5-1 EXIT noun([cat]-[])
4-1 EXIT nounexpression([cat]-[])
3-2 EXIT nounphrase([cat]-[])
2-2 EXIT verbphrase([chases,cat]-[])
1-1 EXIT sentence([dog,chases,cat])
yes

尋找 nani
現在將使用這種分析句法結構的技術,來完成尋找 Nani 。
我們首先假設已經完成以下的兩個任務。第一,已經完成了把使用者的輸入轉換成列表的工作。第二,我們可是使用列表的形式來表示命令,例如, goto(office) 表示成為 [goto,office] ,而 look 表示成為 [look] 。
有了這兩個假設,現在的任務就是把使用者的自然語言轉換成為程式能夠理解的命令列表。例如,我們希望程式能夠把 [go,to,the,office] 轉換成為 [goto,office] 。
最高層的謂詞叫做 command/2 ,它的形式如下:
command(OutputList, InputList).
最簡單的命令就是隻有一個動詞的命令,例如 look 、 list_possessions 和 end 。我們可以使用下面的子句來識別這種命令:
command([V], InList):- verb(V, InList-[]).
我們使用前面介紹過的方法來定義動詞,不過這次將多加入一個引數,這個引數用來構造返回的標準命令列表。為了使這個程式看上去更有趣,我們讓它能夠識別命令多種表達形式。例如結束遊戲可以輸入: end 、 quit 和 good bye 。
下面是幾個簡單的測試:
?- command(X,[look]).
X = [look]
?- command(X,[look,around]).
X = [look]
?- command(X,[inventory]).
X = [list_possessions]
?- command(X,[good,bye]).
X = [end]
下面的任務要複雜一些,我們將考慮動賓結構的命令。使用前面介紹過的知識,可以很容易地完成這個任務。不過此處,還希望除了語法以外還能夠識別語義。
例如, goto 動詞後面所跟隨的物體必須是一個地方,而其他的謂詞後面的賓語則是個物體。為了完成這個任務,我們引入了另一個引數。
下面是主子句,我們可以看出新的引數是如何工作的。
command([V,O], InList) :-
verb(Object_Type, V, InList-S1),
object(Object_Type, O, S1-[]).
還必須用事實來定義一些新的動詞:
verb(place, goto, [go,to|X]-X).
verb(place, goto, [go|X]-X).
verb(place, goto, [move,to|X]-X).
我們甚至可以識別 goto 動詞被隱含的情況,即如果玩家僅僅輸入某個房間的名稱,而沒有前面的謂詞。這種情況下列表及其餘表相同。而 room/1 謂詞則用來檢測列表的元素是否為一個房間,除了房間的名字是兩個單詞的情況。
下面這條規則的意思是:如果我們從列表的頭開始尋找某個動詞,而列表的頭確是一個房間的名稱,那麼就認為找到了動詞 goto ,並且返回完成的列表,好讓後面的操作找到 goto 動詞的賓語。
verb(place, goto, [X|Y]-[X|Y]):- room(X).
verb(place, goto, [dining,room|Y]-[dining,room|Y]).
下面是關於物品的謂詞:
verb(thing, take, [take|X]-X).
verb(thing, drop, [drop|X]-X).
verb(thing, drop, [put|X]-X).
verb(thing, turn_on, [turn,on|X]-X).
有時候,物品前面可能有限定詞,下面的兩個子句考慮的有無限定詞的兩種情況:
object(Type, N, S1-S3) :-
det(S1-S2),
noun(Type, N, S2-S3).
object(Type, N, S1-S2) :-
noun(Type, N, S1-S2).
由於我們處理句子時只需要去掉限定詞,所以就不需要額外的引數。
det([the|X]- X).
det([a|X]-X).
det([an|X]-X).
定義名詞的方法與動詞相似,不過大部分可以使用原來的定義方法,而只有那些兩個單詞以上的名詞才需要特殊的定義方法。位置名詞使用 room 謂詞定義。
noun(place, R, [R|X]-X):- room®.
noun(place, ‘dining room’, [dining,room|X]-X).
location 謂詞和 have 謂詞所定義的東西是物品,這裡我們又必須把兩個單詞的物品單獨定義。
noun(thing, T, [T|X]-X):- location(T,_).
noun(thing, T, [T|X]-X):- have(T).
noun(thing, ‘washing machine’, [washing,machine|X]-X).
我 們可以把對遊戲當前狀態的識別也做到語法中去。例如,我們想做一個可以開關燈的命令,這個命令是 turn_on(light) ,和 turn_on(flashlight) 相對應。如果玩家輸入 turn on the light ,我們必須決定這個 light 是指房間裡的燈還是 flashlight 。
在這個遊戲中,房間的燈是永遠也打不開的,因為玩家所扮演的角色是一個 3 歲的小孩,不過她可以開啟手電筒。下面的程式把 turn on the light 翻譯成 turn on light 或者 turn on flashlight ,這樣就能讓後面的程式來進行判斷了。
noun(thing, flashlight, [light|X], X):- have(flashlight).
noun(thing, light, [light|X], X).

下面來全面的測試一下:
?- command(X,[go,to,the,office]).
X = [goto, office]
?- command(X,[go,dining,room]).
X = [goto, ‘dining room’]
?- command(X,[kitchen]).
X = [goto, kitchen]
?- command(X,[take,the,apple]).
X = [take, apple]
?- command(X,[turn,on,the,light]).
X = [turn_on, light]
?- asserta(have(flashlight)), command(X,[turn,on,the,light]).
X = [turn_on, flashlight]

下面的幾個句子不合法:

?- command(X,[go,to,the,desk]).
no
?- command(X,[go,attic]).
no
?- command(X,[drop,an,office]).
no
Definite Clasue Grammar(DCG)

在 Prolog 中經常用到差異表,因此許多 Prolog 版本都對差異表有很好的支援,這樣就可以隱去差異表的一些繁瑣複雜之處。這種語法稱為 Definite Clasue Grammer(DCG) ,它看上去和一般的 Prolog 子句非常相似,只不過把連線符 :- 替換成為 --> ,這種表達形式由 Prolog 翻譯成為普 通的差異表形式。
使用 DCG ,原來的句子謂詞將寫為:

sentence --> nounphrase, verbphrase.

這個句子將被翻譯成一般的使用差異表的 Prolog 子句,但是這裡不再用“ - ”隔開,而是變成了兩個引數,上面的這個句子與下面的 Prolog 子句等價。

sentence(S1, S2):-
nounphrase(S1, S3),
verbphrase(S3, S2).

因此,既是使用 DCG 形式定義 sentence 謂詞,我們在呼叫時仍然需要兩個引數。

?- sentence([dog,chases,cat], []).

用 DCG 來表示詞彙只需要使用一個列表:

noun --> [dog].
verb --> [chases].

這兩個句子被翻譯成:

noun([dog|X], X).
verb([chases|X], X).

就象在本遊戲中所需要的那樣,有時需要額外的引數來返回語法資訊。這個引數只需要簡單地加入就行了,而句中純 Prolog 則使用 {} 括起來,這樣 DCG 分析器就不會翻譯它。遊戲中的複雜的規則將寫成如下的形式:

command([V,O]) -->
verb(Object_Type, V),
object(Object_Type, O).
verb(place, goto) --> [go, to].
verb(thing, take) --> [take].
object(Type, N) --> det, noun(Type, N).
object(Type, N) --> noun(Type, N).
det --> [the].
det --> [a].
noun(place,X) --> [X], {room(X)}.
noun(place,‘dining room’) --> [dining, room].
noun(thing,X) --> [X], {location(X,_)}.

由於 DCG 自動的取走第一個引數,如果只輸房間名稱,前面的子句就不能起作用,所以我們還要加上一條:
command([goto, Place]) --> noun(place, Place).

讀入句子

讓我們來最後完工吧。最後的工作是把使用者的輸入變成一張表。下面的程式很夠完成這個任務:

% read a line of words from the user
read_list(L) :-
write(’> '),
read_line(CL),
wordlist(L,CL,[]), !.
read_line(L) :-
get0©,
buildlist(C,L).
buildlist(13,[]) :- !.
buildlist(C,[C|X]) :-
get0(C2),
buildlist(C2,X).
wordlist([X|Y]) --> word(X), whitespace, wordlist(Y).
wordlist([X]) --> whitespace, wordlist(X).
wordlist([X]) --> word(X).
wordlist([X]) --> word(X), whitespace.
word(W) --> charlist(X), {name(W,X)}.
charlist([X|Y]) --> chr(X), charlist(Y).
charlist([X]) --> chr(X).
chr(X) --> [X],{X>=48}.
whitespace --> whsp, whitespace.
whitespace --> whsp.
whsp --> [X], {X48}.

它包括兩個部分:首先使用內部謂詞 get0/1 讀入單個的 ASCII 字元, ASCII 13 代表句子結束。第二部分使用 DCG 分析字元列表,從而把它轉化為單詞列表,這裡使用了另一個內部謂詞 name/2 ,它把有 ASCII 字元組成的列表轉化為原子。
另外一部分是把形如 [goto,office] 的命令,轉化為 goto(office) ,我們使用稱為 univ 的內部謂詞完成這個工作,使用 “=…” 表示。它的作用如下,把一個謂詞轉化為了一個列表,或者反過來。

?- pred(arg1,arg2) =… X.
X = [pred, arg1, arg2]
?- pred =… X.
X = [pred]
?- X =… [pred,arg1,arg1].
X = pred(arg1, arg2)
?- X =… [pred].
X = pred
最後我們使用前面的兩個部分做成一個命令迴圈:
get_command© :-
read_list(L),
command(CL,L),
C =… CL, !.
get_command(_) :-
write(‘I don’‘t understand’), nl, fail.
到此為止,我們的 Prolog 教程就全部結束了,但是你的工作沒有結束,如果想很好地掌握這門語言,還有很漫長的路要走。

原文連結