人工智能技術導論——邏輯程序設計語言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 基礎

1、Prolog 的語句

Prolog 語言僅有三種語句:事實(Fact)、規則(Rule)和問題(Question)。數據結構

一、事實

格式:app

<謂詞名>(<項表>).

    謂詞名是以大小寫字母開頭,字母、數字、下劃線等組成的字符串;
    項表是以逗號隔開的項序列。

Prolog 中的項包括由常量或變量表示的簡單對象以及函數、結構和表等,即事實的形式是一個原子謂詞公式。
 
功能:框架

    通常表示對象的性質或關係。

舉個例子:dom

student(john).
like(mary, music).

這就是 Prolog 中兩個合法的事實。分別表示「約翰是學生」和「瑪麗喜歡音樂」。

固然了,做爲特殊情形,一個事實也能夠只有謂詞名而無參量。
舉個例子:模塊化

abc.
repeat.

等也是容許的。函數

二、規則

格式:學習

<謂詞名>(<項表>):-<謂詞名>(<項表>){,<謂詞名>(<項表>)}.

    「:-」號表示「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.

也是一個合法規則。

三、問題

格式:

?-<謂詞名>(<項表>){,<謂詞名>(<項表>)}.

功能:

    問題表示用戶的詢問,它就是程序運行的目標。

舉個例子:

?-student(john).
?-like(mary, X).

分別表示「約翰是學生嗎?」和「瑪麗喜歡誰?」。

問題能夠與規則及事實同時一塊兒給出,也能夠在程序運行時臨時給出。

2、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 程序,其問題就至關於主程序,其規則就至關於子程序,而其事實就至關於數據。

3、Prolog 程序的運行機理

 要想了解 Prolog 的運行機理,首先須要瞭解幾個基本概念。

一、自由變量與約束變量

Prolog中把無值的變量稱爲自由變量,有值的變量稱爲約束變量。一個變量取了某值就說該變量約束於某值,或者說該變量被某值所約束,或者說該變量被某值實例化了。在程序運行期間,一個自由變量能夠被實例化而成爲約束變量,反之,一個約束變量也可被解除其值而成爲自由變量。

二、匹配合一

兩個謂詞可匹配合一,是指兩個謂詞的名相同,參量項的個數相同,參量類型對應相同,而且對應參量項還知足下列條件之一。
    若是兩個都是常量,則必須徹底相同。
    若是兩個都是約束變量,則兩個約束值必須相同。
    若是其中一個是常量,一個是約束變量,則約東值與常量必須相同。
    至少有一個是自由變量。

例以下面的兩個謂詞:

prel("ob1", "ob2", Z) prel("ob1", X, Y)

只有當變量 X 被約束爲"ob2",且 Y、Z 的約束值相同或者至少有一個是自由變量時,它們纔是匹配合一的。
Prolog 的匹配合一,與歸結原理中的合一的意思基本同樣,但這裏的合一同時也是一種操做。這種操做可以使兩個能匹配的謂詞合一塊兒來,即爲參加匹配的自由變量和常量,或者兩個自由變量創建一種對應關係,使得常量做爲對應變量的約束值,使得兩個對應的自由變量始終保持一致,即若其中一個被某值約束,則另外一個也被同一值約束;反之,若其中一個的值被解除,則另外一個的值也被解除。

三、回溯

所謂回溯,就是在程序運行期間,當某一個子目標不能知足(即謂詞匹配失敗)時,控制就返回到前一個已經知足的子目標(若是存在的話),並撤銷其有關變量的約束值,而後再使其從新知足。成功後,再繼續知足原來的子目標。若是失敗的子目標前再無子目標,則控制就返回到該子目標的上一級目標(即該子目標謂詞所在規則的頭部)使它從新匹配。回溯也是 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程序設計。

1、程序結構

一個完整的 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個目標謂詞。這種目標稱爲複合目標。

2、數據與表達式

一、領域

(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)

二、常量與變量

由上面的領域可知, Turbo Prolog的常量有整數、實數、字符、串、符號、結構、表 和 文件 這8種數據類型。同理, Turbo Prolog 的變量也就有這8種取值。另外,變量名要求必須是以大寫字母或下劃線開頭的字母、數字和下劃線 序列,或者只有一個下劃線(這種變量稱爲無名變量)。

三、算術表達式

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 程序設計所必不可少的。這樣,上面的(*)式以及下面的關係表達式稱爲異體謂詞。

四、關係表達式

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 的結果。

3、輸入與輸出

雖然 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

4、分支與循環

Prolog 原本沒有分支和循環語句,但能夠利用其邏輯機制實現分支和循環效果。

一、分支

對於一般的 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.

二、循環

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的遞歸機制也能夠實現循環,不過用遞歸實現循環一般需與表相配合。另外,遞歸的缺點是容易引發內存溢出,故一般的循環可能是用上述方法實現的。

5、動態數據庫

動態數據庫就是在內存中實現的動態數據結構。它由事實組成,程序能夠對它操做,因此在程序運行期間它能夠動態變化。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」中的事實調入內存。

6、表處理與遞歸

一、表頭與表尾

表是 Prolog 中一種很是有用的數據結構。表的表述能力很強,數字中的序列、集合,一般語言中的數組、記錄等都可用表來表示。表的最大特色是其長度不固定,在程序的運行過程當中可動態地變化。具體來說,就是在程序運行時,可對錶施行一些操做,如給表中添加一個元素,或從中刪除一個元素,或者將兩個表合併爲一個表等。用表還能夠方便地構造堆棧、隊列、鏈表、樹等動態數據結構。
表還有一個重要特色,就是它可分爲頭和尾兩部分。表頭是表中第一個元素,而表尾是表中除第一個元素外的其他元素按原來順序組成的表。例以下面的表所示就是一個例子。

 


**表頭與表尾示例** 表

二、表的匹配合一

在程序中是用「|」來區分表頭和表尾的,並且還可使用變量。例如通常地用[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的第一個項是輸入,即原表;第二個項是輸出,即原表的倒置。

7、回溯控制

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。

8、程序舉例

下面給出幾個簡單而又典型的程序實例。經過這些程序,讀者能夠進一步體會和理解 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]
相關文章
相關標籤/搜索