人工智慧技術導論——邏輯程式設計語言PROLOG
最近在複習人工智慧導論,裡面介紹了一種邏輯關係語言PROLOG,但這本書裡面用到的編譯器是Turbo PROLOG,這個編譯器早就被淘汰了,我後來找的了它的升級版Visual PROLOG,但一些語法也發生了變化,現在好像用起來不錯的是SWI PROLOG ,這裡處於複習的目的,把書上關於PROLOG的相關內容儲存到這裡,下面一些程式碼我儘可能的使用SWI PROLOG跑一跑,學習一下。
摘自《人工智慧技術簡明教程》–廉師友 編著
Prolog 概念
Prolog(PROgramming in LOGic的縮寫)語言是一種基於 [Horn 子句的邏輯型程式設計語言,也是一種陳述性語言。 Prolog 與人工智慧的知識表示、自動推理、圖搜尋、產生式系統和專家(知識)系統有著天然的聯絡,很適合智慧程式設計。 若想詳細瞭解可自行百科:http://baike.baidu.com/item/Prolog
今天我們先搞明白 Prolog 語言的基本原理,然後再詳細學習一下 Turbo Prolog 語言程式設計。選擇 Turbo Prolog 是因為它是一個功能較齊全的邏輯程式語言,其程式的可讀性強而且簡單易學,是一個很好的教學語言。另外, Turbo Prolog 程式既可以在 Trubo Prolog 和 PDC Prolog 環境下執行或編譯,又可以在當前流行的視覺化語言 Visual Prolog 的環境下執行或編譯。
Prolog 基礎
一、Prolog 的語句
Prolog 語言僅有三種語句:事實(Fact)、規則(Rule)和問題(Question)。
1、事實
格式:
<謂詞名>(<項表>).
謂詞名是以大小寫字母開頭,字母、數字、下劃線等組成的字串;
項表是以逗號隔開的項序列。
Prolog 中的項包括由常量或變量表示的簡單物件以及函式、結構和表等,即事實的形式是一個原子謂詞公式。
功能:
一般表示物件的性質或關係。
舉個例子:
student(john). like(mary, music).
這就是 Prolog 中兩個合法的事實。分別表示“約翰是學生”和“瑪麗喜歡音樂”。
當然了,作為特殊情形,一個事實也可以只有謂詞名而無參量。
舉個例子:
abc. repeat.
等也是允許的。
2、規則
格式:
<謂詞名>(<項表>):-<謂詞名>(<項表>){,<謂詞名>(<項表>)}.
“:-”號表示“if”(也可以直接寫為 if ),其左部的謂詞是規則的結論(亦稱為頭),右部的謂詞是規則的前提(亦稱為體);
“{}”表示零次或多次重複,逗號表示 and(邏輯與)。
即規則的形式是一個邏輯蘊涵式。
功能:
一般表示物件間的因果關係、蘊含關係或對應關係。
舉個例子:
bird(X) :- animal(X), has(X, feather).
分別表示“如果 X 是動物,並且 X 有羽毛,則 X 是鳥”和“ X 是Y 的祖父,前提是如果存在 Z,X 是 Z 的父親並且 Z 又是 Y 的父親”。
作為特殊情形,規則中的謂詞也可以只有謂詞而無參量。
舉個例子:
run :- start, step1(X), step2(X), end.
也是一個合法規則。
3、問題
格式:
?-<謂詞名>(<項表>){,<謂詞名>(<項表>)}.
功能:
問題表示使用者的詢問,它就是程式執行的目標。
舉個例子:
?-student(john). ?-like(mary, X).
分別表示“約翰是學生嗎?”和“瑪麗喜歡誰?”。
問題可以與規則及事實同時一起給出,也可以在程式執行時臨時給出。
二、Prolog 的程式
Prolog 程式一般由一組事實、規則和問題組成。問題是程式執行的起點,稱為程式的目標。
我們首先寫出一個 Prolog 的程式,如下:(為引用方便起見,我們把這個程式稱為“程式0”)
likes(bell, sports). likes(mary, music). likes(mary, sports). likes(jane, smith). friend(john, X) :- likes(X, reading), likes(X, music). friend(john, X) :- likes(X, sports), likes(X, music). ?- friend(john, Y).
接下來我們分析一下這個程式:
可以看出,這個程式中有四個事實、兩條規則和一個問題。其中事實、規則和問題都分行書寫;規則和事實可連續排列在一起,其順序可隨意安排,但同一謂詞名的事實或規則必須集中排列在一起;問題不能與規則及事實排在一起,它作為程式的目標要麼單獨列出,要麼在程式執行時臨時給出。
這個程式的事實描述了一些物件(包括人和事物)間的關係;而規則則描述了
John 交朋友的條件,即如果一個人喜歡讀書並且喜歡音樂(或者喜歡運動和喜歡音樂),那麼這個人就是 John 的朋友(當然,這個規則也可看做
John 朋友的定義);程式中的問題是“約翰的朋友是誰?"
prolog執行結果:
Prolog 程式中的目標還可以變化,也可以含有多個語句(上例中只有一個)。如果有多個語句,則這些語句稱為子目標。例如對上面的程式,其問題也可以是:
?-likes(mary, X).
或
?-likes(mary, music).
或
?-friend(X, Y).
或
?-likes(bell, sports),likes(mary, music),friend(john, X).
等。但對於不同的問題,程式執行的結果一般是不一樣的。
還需說明的是,Prolog程式中的事實或規則一般稱為它們對應謂詞的子句。例如,上面程式中的前4句都是謂詞
likes 的子句。 Prolog 規定,同一謂詞的子句應排在一起。從語句形式和程式組成來看, Prolog 就是一種基於 Horn
子句的邏輯程式。這種程式要求用事實和規則來求證詢問,即證明所給出的條件子句和無條件子句與目標子句是矛盾的,或者說程式中的子句集是不可滿足的。這就是所謂的
Prolog 的說明性語義。
從 Prolog 的語句來看, Prolog 語言的文法結構相當簡單。但由於它的語句是 Horn
子句,而 Horn 子句的描述能力是很強的,所以 Prolog
的描述能力也是很強的。例如,當它的事實和規則描述的是某一學科的公理,那麼問題就是待證的命題;當事實和規則描述的是某些資料和關係,那麼問題就是資料查詢語句;當事實和規則描述的是某領域的知識,那麼問題就是利用這些知識求解的問題;當事實和規則描述的是某初始狀態和狀態變化規律,那麼問題就是目標狀態。所以,
Prolog 語言實際是一種應用相當廣泛的智慧程式設計語言。從上面最後一個目標可以看出,同過程性語言相比,對於一個 Prolog
程式,其問題就相當於主程式,其規則就相當於子程式,而其事實就相當於資料。
三、Prolog 程式的執行機理
要想了解 Prolog 的執行機理,首先需要了解幾個基本概念。
1、自由變數與約束變數
Prolog中把無值的變數稱為自由變數,有值的變數稱為約束變數。一個變數取了某值就說該變數約束於某值,或者說該變數被某值所約束,或者說該變數被某值例項化了。在程式執行期間,一個自由變數可以被例項化而成為約束變數,反之,一個約束變數也可被解除其值而成為自由變數。
2、匹配合一
兩個謂詞可匹配合一,是指兩個謂詞的名相同,參量項的個數相同,參量型別對應相同,並且對應參量項還滿足下列條件之一。
如果兩個都是常量,則必須完全相同。
如果兩個都是約束變數,則兩個約束值必須相同。
如果其中一個是常量,一個是約束變數,則約東值與常量必須相同。
至少有一個是自由變數。
例如下面的兩個謂詞:
prel("ob1", "ob2", Z) prel("ob1", X, Y)
只有當變數 X 被約束為"ob2",且 Y、Z 的約束值相同或者至少有一個是自由變數時,它們才是匹配合一的。
Prolog
的匹配合一,與歸結原理中的合一的意思基本一樣,但這裡的合一同時也是一種操作。這種操作可使兩個能匹配的謂詞合一起來,即為參加匹配的自由變數和常量,或者兩個自由變數建立一種對應關係,使得常量作為對應變數的約束值,使得兩個對應的自由變數始終保持一致,即若其中一個被某值約束,則另一個也被同一值約束;反之,若其中一個的值被解除,則另一個的值也被解除。
3、回溯
所謂回溯,就是在程式執行期間,當某一個子目標不能滿足(即謂詞匹配失敗)時,控制就返回到前一個已經滿足的子目標(如果存在的話),並撤銷其有關變數的約束值,然後再使其重新滿足。成功後,再繼續滿足原來的子目標。如果失敗的子目標前再無子目標,則控制就返回到該子目標的上一級目標(即該子目標謂詞所在規則的頭部)使它重新匹配。回溯也是
Prolog 的一個重要機制。
下面有例子,看完這個 Prolog 程式的執行過程就懂了。
有了上面的基本概念,下面就介紹所 Prolog 程式的執行過程。我們仍以上面給出的 Prolog 程式為例。
設所給的詢問是:
?-friend(john, Y). (john和誰是朋友?)
則求解目標為:
friend(john, Y).
這時,系統對程式進行掃描,尋找能與目標謂詞匹配合一的事實或規則頭部。顯然,程式中前面的 4 個事實均不能與目標匹配,而第 5 個語句的左端即規則為:
friend(john, Y) :- likes(X, reading), likes(X, music).
的頭部可與目標謂詞匹配合一。但由於這個語句又是一個規則,所以其結論要成立則必須其前提全部成立。於是,對原目標的求解就轉化為對新目標:
likes(X, reading), likes(X, music).
的求解。這實際是經過歸結,規則頭部被消去,而目標子句變為:
?- likes(X, reading), likes(X, music).
現在依次對子目標:
likes(X, reading)和 likes(X, music)
求解。
子目標的求解過程與主目標完全一樣,也是從頭對程式進行掃描,不斷進行測試和匹配合一等,直到匹配成功或掃描完整個程式為止。
可以看出,對第一個子目標 like(X, reading)的求解因無可匹配的事實和規則而立即失敗,進而導致規則:
friend(john, X) :- likes(X, reading), likes(X, music).
的整體失敗。於是,剛才的子目標:
likes(X, reading)和 likes(X, music)
被撤銷,系統又回溯到原目標 friend(john, X)。這時,系統從該目標剛才的匹配語句處(即第 5 句)向下繼續掃描程式中的子句,試圖重新使原目標匹配,結果發現第 6 條語句的左部,即規則
friend(john, X) :- likes(X, sports), likes(X, music).
的頭部可與目標謂詞匹配。但由於這個語句又是一個規則,於是,這時對原目標的求解就又轉化為依次對子目標:
likes(X, sports)和 likes(X, music)
的求解。這次子目標 likes(X, sports)與程式中的事實立即匹配成功,且變數 X 被約束為 bell。於是,系統便接著求解第 2 個子目標。由於變數 X 已被約束,所以這時第 2 個子目標實際上已變成:
likes(bell, music).
由於程式中不存在事實 likes(bell, music),所以該目標的求解失敗。於是,系統就放棄這個子目標,並使變數 X 恢復為自由變數,然後回溯到第一個子目標,重新對它進行求解。由於系統已經記住了剛才已同第一子目標謂詞匹配過的事實的位置,所以重新求解時,便從下一個事實開始測試。易見,當測試到程式中的第 3 個事實時,第一個子目標便求解成功,且變數 X 被約束為 mary 。這樣,第 2 個子目標也就變成:
likes(mary, music).
再對它進行求解。這次很快成功。
由於兩個子目標都求解成功,所以,原目標 friend(john, Y)也成功,且變數 Y 被約束為 mary(由 Y 與 X 的合一關係)。於是,系統回答:
Y = mary
程式執行結束。上述程式的執行過程如圖下所示。
Prolog 程式執行機理圖解示例
上述程式的執行是一個通過推理實現的求值過程。我們也可以使它變為證明過程。例如,把上述程式中的詢問改為:
friend(john, mary).
則系統會回答“yes”。
若將詢問改為:
friend(john, smith).
則系統會回答“no”。
這其實也和我們在推理證明中經常問的一個問題:“是不是?"和"是什麼?“,二者的難度是一樣的!
從上述程式的執行過程來看,
Prolog
程式的執行過程是一個(歸結)演繹推理過程。其推理方式為反向推理,控制策略是深度優先且有回溯機制,具體實現方法是:自上而下匹配子句;從左向右選擇子目標;(歸結後)產生的新子目標總是插入被消去的目標處(即目標佇列的左部)。Prolog
的這種歸結演繹方法被稱為 SLD(Linear resolution with Selection function for Definite
clause)歸結, 或 SLD 反駁 - 消解法。這樣,SLD 歸結就是 Prolog 程式的執行機理,也就是所謂的 Prolog
語言的過程性語義。
Turbo Prolog 程式設計
Turbo Prolog 是一個編譯型語言,1986
年由美國的 BORLAND 公司推出,執行在 IBM PC 系列機及其相容機上。Turbo prolog 除了具有基本 prolog
的邏輯程式特點外還具有速度快、功能強,具有整合化開發環境,可同其他語言介面,能實現動態資料庫和大型外部資料庫,可直接訪問機器系統硬軟體和具有圖形、視窗功能等一系列特點。
本次我們以 Turbo prolog(2.0版)為例,介紹 prolog程式設計。
一、程式結構
一個完整的 Turbo prolog 程式一般包括常量段、領域段、資料庫段、謂詞段、目標段和子句段 6 個部分。各段以其相應的關鍵字 constants、domains、database、predicates、goal 和 clauses 開頭加以標識。另外,在程式的首部還可以設定指示編譯程式執行特定任務的編譯指令;在程式的任何位置都可設定註解。總之,一個完整的 Turbo prolog(2.0版)程式的結構如下:
/* < 注 釋 > */ <編譯指令> constants <常量說明> domains <域說明> database <資料庫說明> predicates <謂詞說明> goal <目標語句> clauses <子句集>
當然,一個程式不一定要包括上述所有段,但一個程式至少要有一個 predicates 段、clauses 段和 goal 段。在大多數情形中,還需要一個 domains段,以說明表、複合結構及使用者自定義的域名。如若省略 goal 段,則可在程式執行時臨時給出,但這僅當在開發環境中執行程式時方可給出。若要生成一個獨立的可執行檔案,則在程式中必須包含 goal 段。另外,一個程式也只能有個 goal 段。
在模組化程式設計中,可以在關鍵字 domains, predicates 及 database前加上 global,以表明相應的說明是全域性的,以便作用於幾個程式模組。
舉個例子,如果要將上一節的 Prolog 程式作為 Turbo prolog程式,則應改寫為
DOMAINS name = symbol PREDICATES likes(name, name) friend(name, name) GOAL friend(john,Y), write("Y = ", Y). CLAUSES likes(bell, sports). likes(mary, music). likes(mary, sports). likes(jane, smith). friend(john, X) :- likes(X, sports), likes(X, music). friend(john, X) :- likes(X, reading), likes(X, music).
下面對程式結構中的幾個段作以說明。
領域段 該段說明程式謂詞中所有參量項所屬的領域。領域的說明可能會出現多層說明,直到說明到 Turbo prolog 的標準領域為止(如上例所示)。Turbo prolog 的標準領域即標準資料型別,包括整數、實數、字元、串和符號等,其具體說明如下表所示。
Turbo Prolog 的標準領域表
謂詞段 該段說明程式中用到的謂詞的名和參量項的名(但 Turbo prolog的內部謂詞無須說明)。
子句段 該段是 Turbo prolog 程式的核心,程式中的所有事實和規則就放在這裡,系統在試圖滿足程式的目標時就對它們進行操作。
目標段 該段是放置程式目標的地方。目標段可以只有一個目標謂詞,例如上面的例子中就只有一個目標謂詞;也可以含有多個目標謂詞,如:
goal readint(X), Y = X+3, write("Y ='%Y).
就有3個目標謂詞。這種目標稱為複合目標。
二、資料與表示式
1、領域
(1)標準領域。
Turbo prolog中不定義變數的型別,只說明謂詞中各個項的取值域。由上面我們知道, Turbo prolog 有整數、實數、字元、串 和 符號這5種標準域。另外,它還有結構、表和檔案這3種複合域。
(2)結構。
結構也稱複合物件,它是 Turbo prolog 謂詞中的一種特殊的參量項(類似於謂詞邏輯中的函式)。結構的一般形式為:
<函子>(<參量表>)
其中函子及參量的識別符號與謂詞相同。注意,這意味著結構中還可包含結構。所以,複合物件可表達樹形資料結構。例如下面的謂詞:
likes("Tom", sports(football, basketball, table_tennis)).
中的
sports(football, basketball, table_tennis)
就是一個結構,即複合物件。又如:
person("張華", student("清華大學"), address("中國", "北京")). reading("王巨集", book("人工智慧技術導論", "西安電子科技大學出版社")). friend(father("Li"), father("Zhao")).
這幾個謂詞中都有複合物件。
複合物件在程式中的說明,需分層進行。例如,對於上面的謂詞:
likes("Tom", sports(football, basketball, table_tennis)).
在程式中可說明如下:
domains name = symbol sy = symbol sp = sports(sy, sy, sy) predicates likes(name, sp)
(3)表。
表的一般形式是:
[x1, x2, ..., xn]
其中xi(i=1,2,...,n)為 Prolog 的項,一般要求同一個表的元素必須屬於同一領域。不含任何元素的表稱為空表,記為[]。例如下面就是一些合法的表。
[1,2,3] [ apple, orange, banana, grape, cane] ["Prolog", "MAENS", "PROGRAMMING", "in logic"] [[a, b], [c, d], [e]] []
表的最大特點是其元素個數可在程式執行期間動態變化。表的元素也可以是結構或表,且這時其元素可以屬於不同領域。例如:
[name("LiMing"), age(20), sex(male), address(xian)] [[1, 2], [3, 4, 5], [6, 7]]
都是合法的表。後一個例子說明,表也可以巢狀。
實際上,表是一種特殊的結構,它是遞迴結構的另一種表達形式。這個結構的函式名取決於具體的 Prolog 版本,這裡我們就用一個圓點來表示。下面就是一些這樣的結構及它們的表的表示形式:
表的說明方法是在其組成元素的說明符後加一個星號*。如:
domains lists = string* predicates pl(lists)
就說明謂詞 pl 中的項 lists 是一個由串 string 組成的表。
對於由結構組成的表,至少分3步說明。例如對於下面謂 p 中的表
p([name("Liming"), age(20)])
則需這樣說明:
domains rec=seg* seg=name(string); age(integer) predicates p(rec)
2、常量與變數
由上面的領域可知, Turbo Prolog的常量有整數、實數、字元、串、符號、結構、表 和 檔案 這8種資料型別。同理, Turbo Prolog 的變數也就有這8種取值。另外,變數名要求必須是以大寫字母或下劃線開頭的字母、數字和下劃線 序列,或者只有一個下劃線(這種變數稱為無名變數)。
3、算術表示式
Turbo Prolog 提供了 5 種最基本的算術運算:加、減、乘、除 和 取模,相應運算子號為“+”、“-”、“*”、“/”、“mod”。這 5 種運算的順序為:“*”、“/”、“mod”優先於“+”、“-”。同級從左到右按順序運算,括號優先。
算術表示式的形式與數學中的形式基本一樣。例如:
數學中的算術表示式 Turbo Prolog 中的算術表示式
即, Turbo Prolog 中算術表示式採用通常數學中使用的中綴形式。這種算術表示式為 Prolog 的一種異體結構,若以 Prolog 的結構形式來表示,則它們應為:
+(X, *(Y, Z)) -(*(A, B), /(C, D)) mod(U, V)
所以,運算子“+”、“-”、“*”、“/”和“mod”實際也就是 Prolog 內部定義好了的函式符。
在
Turbo Prolog
程式中,如果一個算術表示式中的變元全部被例項化(即被約束),則這個算術表示式的值就會被求出。求出的值可用來例項化某變數,也可用來同其他數量進行比較,用一個算術表示式的值例項化一個變數的方法是用謂詞“is”或“=”來實現的。例如:
Y is X + 5
或
Y = X + 5 (*)
就使變數 Y 例項化為 X+5 的值(當然 X 也必須經已被某值例項化),可以看出,這裡對變數 Y 的例項化方法類似於其他高階程式語言中的“賦值”,但又不同於賦值。例如,在 Prolog 中下面的式子是錯誤的:
X = X + 1
需要說明的是,雖然 Prolog 是一種邏輯程式設計語言,但在目前的硬體條件下卻非突破邏輯框架不可。這是因為有些實用操作是無法用邏輯描述的(如輸入與輸出),有些算術運算在原則上可用邏輯描述,但這樣做效率太低。為此, Prolog 提供了若干內部謂詞(亦稱 預定義謂詞),來實現算術運算、輸入與輸出等操作。所謂內部謂詞,就是 Prolog 的解釋程式中,預先用實現語言定義好的使用者可直接作為子目標呼叫的謂詞。一般的 Prolog 實用系統都配有 100 個以上的內部謂詞,這些內部謂詞涉及輸入輸出、算術運算、搜尋控制、檔案操作和圖形聲音等方面,它們是實用 Prolog 程式設計所必不可少的。這樣,上面的(*)式以及下面的關係表示式稱為異體謂詞。
4、關係表示式
Turbo Prolog 提供了 6 種常用的關係運算,即 小於、小於或等於、等於、大於、大於或等於、不等於,其運算子依次為:
<, <=, =, >, >=, <>
Turbo Prolog 的關係表示式的形式和數學中的也基本一樣,例如:
即, Turbo Prolog 中的關係式也用中綴形式。當然,這種關係式為 Turbo Prolog 中的異體原子。若按 Turbo Prolog 中的原子形式來表示,則上面的兩個例子為:
>=(X +1, Y) 和 <>(X, Y)
所以上述 6 種關係運算符,實際上也就是 Turbo Prolog 內部定義好了的 6 個謂詞。這 6 個關係運算符可用來比較兩個算術表示式的大小。例如:
brother(Name1, Name2) :- person(Name1, man, Age1), person(Name2, man, Age2), mother(Z, Name1), mother(Z, Name2), Age1 > Age2.
需要說明的是,“=”的用法比較特殊,它既可以表示比較,也可以表示約束值,即使在同一個規則中的同一個“=”也是如此。例如:
p(X, Y, Z) :- Z = X + Y.
當變數 X、Y、Z全部被例項化時,“=”就是比較符。如對於問題:
Goal: p(3, 5, 8).
機器回答“yes”,而對於:
Goal: p(3, 5, 7).
機器回答“no”。即這時機器把 X+Y 的值與Z的值進行比較。但當 X,Y 被例項化,而 Z 未被例項化時, “=”號就是約束符,如:
Goal: P(3, 5, Z).
機器回答“Z = 8”。這時,機器使 Z 例項化為 X+Y 的結果。
三、輸入與輸出
雖然 Prolog 能自動輸出目標子句中的變數的值,但這種輸出功能必定有限,往往不能滿足實際需要;另外,對通常大多數的程式來說,執行時從鍵盤上輸人有關資料或資訊也是必不可少的。為此每種具體 Prolog 一般都提供專門的輸人和輸出謂詞,供使用者直接呼叫。例如,下面就是 Turbo Prolog 的幾種輸入輸出謂詞:
readin(X)。這個謂詞的功能是從鍵盤上讀取一個字串,然後約束給變數 X 。
readint(X)。這個謂詞的功能是從鍵盤上讀取一個整數,然後約束給變數 X,如果鍵盤上打入的不是整數,則該謂詞失敗。
readreal(X)。這個謂詞的功能是從鍵盤上讀取一個實數,然後約束給變數 X,如果鍵盤上打入的不是實數,則該謂詞失敗。
readchar(X)。這個謂詞的功能是從鍵盤上讀取一個字元,然後約束給變數 X,如果鍵盤上打入的不是單個字元,則該謂詞失敗。
write(X1, X2, …, Xn)。這個謂詞的功能是把項 Xi(i=1,2,…,n) 的值顯示在螢幕上或者列印在紙上,當有某個 Xi 未例項化時,該謂詞失敗。其中的 Xi 可以是變數,也可以是字串或數字。例如:
write("computer", "Prolog", Y, 1992)
nl(換行謂詞)。它使後面的輸出(如果有的話)另起一行。另外,利用 write的輸出項“\n”也同樣可起到換行作用。例如:
write("name"), nl, write("age")
與
write("name", "\n", "age")
的效果完全一樣。
舉個例子:
用上面的輸入輸出謂詞編寫一個簡單的學生成績資料庫查詢程式。
PREDICATES student(integer, string, real) grade GOAL grade. CLAUSES student(1, "張三", 90.2). student(2, "李四", 95.5). student(3, "王五", 96.4). grade :- write("請輸人姓名:"), readln(Name), student(_, Name, Score), nl, write(Name, "的成績是", Score). grade :- write("對不起,找不到這個學生!").
下面是程式執行時的螢幕顯示
請輸入姓名:王五 王五的成績是96.4
四、分支與迴圈
Prolog 本來沒有分支和迴圈語句,但可以利用其邏輯機制實現分支和迴圈效果。
1、分支
對於通常的 IF-THEN-ELSE 分支結構,Prolog可用兩條同頭的(同頭即指結論相同)並列規則實現。例如,將:
IF X>0 THEN X:=1 ELSE X:=0
用 Prolog實現則是:
br :- X > 0, X = 1. br :- X = 0.
類似地,對於多分支,可以用多條規則實現。例如:
br :- X > 0, X = 1. br :- X = 0, X = 0. br :- X < 0, X = -1.
2、迴圈
Prolog 可以實現計迴圈次數的 FOR 迴圈,也可以實現不計迴圈次數的 DO 迴圈。
舉個例子:
下面的程式段就實現了迴圈,它使得 write 語句重複執行了3次,而列印輸出了3個學生的記錄:
student(1, "張三", 90.2). student(2, "李四", 95.5). student(3, "王五", 96.4). print :- student(Number, Name, Score), write(Number, Name, Score), nl, Number = 3.
可以看出,程式第一次執行,student 謂詞與第一個事實匹配,write 語句便輸出了張三同學的記錄。但當程式執行到最後一句時,由於 Number 不等於 3,則該語句失敗,於是,引起回溯。而 write 語句和 nl 語句均只能執行一次,所以繼續向上回溯到 student 語句。這樣,student 謂詞則因失敗而重新匹配。這一次與第二個事實匹配,結果輸出了李四的記錄。同理,當執行到最後一句時又引起了回溯。write 語句第三次執行後,由於 Number 已等於3,所以最後一個語句成功,程式段便執行結束。
這個例子可以看做是計數迴圈。當然,也可以通過設定計數器而實現真正的計數迴圈。下面的程式段實現的則是不計數的 DO 迴圈:
student(1, "張三", 90.2). student(2, "李四", 95.5). student(3, "王五", 96.4). print :- student(Number, Name, Score), write(Number, Name, Score), nl, fail. print :-.
這個程式段中的 fail 是一個內部謂詞,它的語義是恆失敗。這個程式段與上面的程式段的差別僅在於把原來用計數器(或標記數)進行迴圈控制的語句變成了恆失敗謂詞 fail,另外又增加了一個 print 語句,增加這個語句的目的是為程式設定一個出口。因為 fail 是恆失敗,下面若無出口的話,將引起 print 本身的失敗,進而又會導致程式的連鎖失敗。
還需說明的是,用 Prolog的遞迴機制也可以實現迴圈,不過用遞迴實現迴圈通常需與表相配合。另外,遞迴的缺點是容易引起記憶體溢位,故通常的迴圈多是用上述方法實現的。
五、動態資料庫
動態資料庫就是在記憶體中實現的動態資料結構。它由事實組成,程式可以對它操作,所以在程式執行期間它可以動態變化。Turbo Prolog 提供了 3 個動態資料庫操作謂詞,即:
asserta(< fact >) assertz(< fact >) retract(< fact >)
其中 fact 表示事實。這 3 個謂詞的功能如下:
asserta(< fact >) 把 fact 插入當前動態資料庫中的同名謂詞的事實之前。
assertz(< fact >) 把 fact 插入當前動態資料庫中的同名謂詞的事實之後。
retract(< fact >) 把 fact 從當前動態資料庫中刪除。
例如語句:
asserta(student(20, "李明", 90.5)).
將在記憶體的謂詞名為 student 的事實前插入一個新事實:
student(20, ''李明", 90.5)
如果記憶體中還沒有這樣的事實,則它就是第一個。又如語句:
retract(student(20, _, _)).
將從記憶體的動態資料庫中的刪除事實:
student(20, _, _)
它可解釋為學號為 20 的一個學生的記錄。注意,這裡用了無名變數 “_”。
可以看出,Turbo Prolog 提供的動態資料庫機制,可非常方便地實現堆疊、佇列等動態資料結構,提供的資料庫操作謂詞大大簡化了程式設計。
另外,Turbo Prolog 還提供了兩個檔案操作謂詞:
save(< filename >). consult(< filename >).
其中 save 可將當前記憶體中的事實存入檔案“filename”中,consult 則將檔案“filename”中的事實調入記憶體。
六、表處理與遞迴
1、表頭與表尾
表是 Prolog 中一種非常有用的資料結構。表的表述能力很強,數字中的序列、集合,通常語言中的陣列、記錄等均可用表來表示。表的最大特點是其長度不固定,在程式的執行過程中可動態地變化。具體來講,就是在程式執行時,可對錶施行一些操作,如給表中新增一個元素,或從中刪除一個元素,或者將兩個表合併為一個表等。用表還可以方便地構造堆疊、佇列、連結串列、樹等動態資料結構。
表還有一個重要特點,就是它可分為頭和尾兩部分。表頭是表中第一個元素,而表尾是表中除第一個元素外的其餘元素按原來順序組成的表。例如下面的表所示就是一個例子。
**表頭與表尾示例** 表
2、表的匹配合一
在程式中是用“|”來區分表頭和表尾的,而且還可以使用變數。例如一般地用[H|T]來表示一個表,其中 H、T 都是變數,H 為表頭,T為表尾。注意,此處 H 是一個元素(表中第一個元素),而 T 則是一個表(除第一個元素外表中的其餘元素按原來順序組成的表)。表的這種表示法很有用,它為表的操作提供了極大的方便。如下面的表所示即為用這種表示法通過匹配合一提取表頭和表尾的例子。
**表的匹配合一示例** 表
還需說明的是,表中的“|”後面只能有一個變數。例如寫法 [X | Y, Z] 就是錯誤的。但豎槓前面的變數可以多於一個。例如寫法 [ X, Y | Z] 是允許的。這樣,這個表同 [a, b, c] 匹配合一後,有:
X = a, Y = b, Z = [c]
另外,豎槓的前面和後面也可以是常量,例如 [a | Y] 和 [X | b] 都是允許的,但需要注意,後一個表稱為無尾表,如果它同表 [a | Y] 匹配,則有:
X = a, Y = b (而不是 Y = [b])
如果無“|”,則不能分離出表尾。例如,表 [X, Y, Z] 與 [a, b, c] 合一後得 X = a,Y = b,Z = c,其中變數 Z 並非等於 [c] 。
接下來我們通過三個例子來更詳細地瞭解表的操作。
設計一個能判斷物件 X 是表 L 的成員的程式。
我們可以這樣設想:
如果 X 與表 L 中的第一個元素(即表頭)是同一個物件,則 X 就是 L的成員;
如果 X 是 L 的尾部的成員,則 X 也就是 L 的成員。
根據這種邏輯關係,有下面的 Prolog 程式:
member(X, [X | Tail]). member(X, [Head | Tail]) :- member(X, Tail).
其中第一個子句的語義就是上面的第一句話;第二個子句的語義就是上面的第二句話。可以看出,這個程式中使用了遞迴技術,因為謂詞 member 的定義中又含有它自身。利用這個程式就可以判定任意給定的一個物件和一個表之間是否具有 member(成員)關係。例如,取表 L 為 [a, b, c, d],取 X 為 a,對上面的程式提出如下詢問:
Goal : member(a, [a, b, c, d]).
則回答“yes”。同樣對於詢問:
Goal : member(b, [a, b, c, d]). Goal : member(c, [a, b, c, d]). Goal : member(d, [a, b, c, d]).
均回答“yes”。但若詢問:
Goal : member(e, [a, b, c, d]).
則回答“no”。如果我們這樣詢問:
Goal : member(X, [a, b, c, d]).
意思是要證明存在這樣的 X,它是該表的成員,這時系統返回 X 的值,即:
X = a
如果需要的話,系統還會給出 X 的其他所有值。
寫一個表的拼接程式,即把兩個表連線成一個表。
append([], L, L). append([H | T], L2, [H | Tn]) :- append(T, L2, Tn).
程式中第一個子句的意思是空表同任一表 L 拼接的結果仍為表 L;第二個子句的意思是說,一個非空的表 L1 與另一個表 L2 拼接的結果 L3 是這樣一個表,它的頭是 L1 的頭,它的尾是由 L1 的尾 T 同 L2 拼接的結果 Tn。這個程式刻畫了兩個表與它們的拼接表之間的邏輯關係。
可以看出,謂詞 append 是遞迴定義的,子句append([], L, L).為終結條件即遞迴出口。
對於這個程式,如果我們詢問:
Goal : append([1, 2, 3], [4, 5], L).
則系統便三次遞迴呼叫程式中的第二個子句,最後從第一個子句終止,然後反向依次求出每次的拼接表,最後輸出:
L=[1, 2, 3, 4, 5]
當然,對於這個程式也可以給出其他各種詢問,如:
Goal : append([1, 2, 3], [4, 5], [1, 2, 3, 4, 5]).
系統回答yes。
Goal : append([1, 2, 3], [4, 5], [1, 2, 3, 4, 5, 6]).
系統回答no。
Goal : append([1, 2, 3], Y, [1, 2, 3, 4, 5]).
系統回答X = [4, 5]。
Goal : append(X, [4, 5], [1, 2, 3, 4, 5]).
系統回答X = [1, 2, 3]。
Goal : append(X, Y, [1, 2, 3, 4, 5]).
系統回答
X = [], Y = [1, 2, 3, 4, 5] X = [1], Y = [2, 3, 4, 5] X = [1, 2], Y = [3, 4, 5] X = [1, 2, 3], Y = [4, 5]
等(如果需要所有解的話)。
表的輸出
print([]). print([H | T]) :- write(H), print(T).
表的倒置,即求一個表的逆序表。
reverse([], []). reverse([H | T], L) :- reverse(T, L1), append(L1, [H], L).
這裡,reverse的第一個項是輸入,即原表;第二個項是輸出,即原表的倒置。
七、回溯控制
Prolog 在搜尋目標解的過程中,具有回溯機制,即當某一個子目標“Gi”不能滿足時,就返回到該子目標的前一個子目標“Gi-1”,並放棄“Gi-1”的當前約束值,使它重新匹配合一。在實際問題中,有時卻不需要回溯,為此 Prolog 中就專門定義了一個阻止回溯的內部謂同——“!”,稱為截斷謂詞。
截斷謂詞的語法格式很簡單,就是一個感嘆號“!”。! 的語義如下。
若將“!”插在子句體內作為一個子目標,它總是立即成功。
若“!”位於子句體的最後,則它就阻止對它所在子句的頭謂詞的所有子句的回溯訪向,而讓回溯跳過該頭謂詞(子目標),去訪問前一個子目標(如果有的話)。
若“!”位於其他位置,則當其後發生回溯且回溯到“!”處時,就在此處失敗,並且“!”還使它所在子句的頭謂詞(子目標)整個失敗(即阻止再去訪問頭謂詞的其餘子向(如果有的話),即迫使系統直接回溯到該頭謂詞(子目標)的前一個子目標(如果有的話))。
舉個例子:
考慮下面的程式
p(a). (7 - 1) p(b). (7 - 2) q(b). (7 - 3) r(X) :- p(X), q(X). (7 - 4) r(c).
對於目標:r(X).可有一個解:
Y = b
但當我們把式(7 - 4)改為:
r(X) :- p(X), !, q(X). (7 - 4')
時,卻無解。為什麼?
這是由於添加了截斷謂詞“!”。因為式(7
- 4’)中求解子目標 p(X) 時,X 被約束到 a,然後跳過“!”,但在求解子目標 q(a)
時遇到麻煩,於是又回溯到“!”,而“!”阻止了對 p(X)的下一個子句 p(b) 和 r 的下一個定義子句 r© 的訪問。從而導致整個求解失敗。
再舉個例子:
設有程式:
g0 :- g11, g12, g13. (7 - 5) g0 :- g14. (7 - 6) g12 :- g21, !, g23. (7 - 7) g12 :- g24, g25. (7 - 8) ... ...
給出目標:g0。
假設執行到子目標 g23 時失敗,這時如果子句(7 -
7)中無“!”的話,則會回溯到 g21,並且如果 g21 也失敗的話,則會訪問下面的子句(7 - 8)。但由於有“!”存在,所以不能回溯到
g21,而直接宣告 g12 失敗。於是由子句(7 - 5),這時則回溯到 g11。如果我們把子句(7 - 7)改為:
g12 :- g21, g23, !. (7 - 9)
當然這時若 g23 失敗時,便可回溯到 g21,而當 g21 也失敗時,便回溯到 g12,即子句(7 - 8)被“啟用”。但對於修改後的程式,如果 g13 失敗,則雖然可回溯到 g12,但對 g12 不做任何事情便立即跳過它,而回溯到 g11。如果子句(7 - 9)中無“!”,則當 g13 失敗時,回溯到 g12 便去考慮子句(7 - 8),只有當子句(7 - 8)再失敗時才回溯到 g11。
八、程式舉例
下面給出幾個簡單而又典型的程式例項。通過這些程式,讀者可以進一步體會和理解 Prolog 程式的風格和能力,也可以掌握一些基本的程式設計技巧。
例8-1:下面是一個簡單的路徑查詢程式。程式中的事實描述瞭如下圖所示的有向圖,規則是圖中兩節點間有通路的定義。
predicates road(symbol, symbol) path(symbol, symbol) clauses road(a, b). road(a, c). road(b, d). road(c, d). road(d, e). road(b, e). path(X, Y) :- road(X, Y). path(X, Y) :- road(X, Z), path(Z, Y).
程式中未含目標,所以執行時需給出外部目標。例如當給出目標:
path(a, e).
時,系統將回答yes,但當給出目標:
path(e, a).
時,系統則回答no,如果給出目標:
run.
且在程式中增加子句:
run :- path(a, X), write("X =", X), nl, fail. run.
螢幕上將會輸出:
X = b X = c X = d X = e X = d X = e X = e
即從 a 出發到其他節點的全部路徑。
例8-2:下面是一個求階乘程式,程式中使用了遞迴。
/* a Factorial Program */ domains n, f = integer predicates factorial(n, f) goal readint(I), factorial(I, F), write(I, "!=", F). clauses factorial(1, 1). factorial(N, Res) :- N > 0, N1 = N - 1, factorial(N1, FacN1), Res = N * FacN1.
程式執行時,從鍵盤上輸入一個整數,螢幕上將顯示其階乘數。
例8-3:下面是一個表的排序程式,採用插入排序法。
/* insert sort */ domains listi = integer* predicates insert_sort(listi, listi) insert(integer, listi, listi) asc_order(integer, integer) clauses insert_sort([], []). insert\_sort([H | Tail], Sorted_list) :- insert_sort(Tail, Sorted\_Tail), insert(H, Sorted_Tial, Sorted\_list). insert(X, [Y | Sorted_list1]) :- asc_order(X, Y), !, insert(X, Sorted_list, Sorted\_list1). insert(X, Sorted_list, [X | Sorted\_list]). asc_order(X, Y) :- X > Y.
程式中對錶的處理使用了遞迴。程式中也未給出目標,需要在執行時臨時給出。例如當給出目標:
insert_sort([5, 3, 4, 2, 6, 1, 7, 8, 9, 0], L).
時,系統將輸出:
L = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]