最近在複習人工智能導論,裡面介紹了一種邏輯關系語言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]
作者:王陸