Prolog Tutorial Cn
Prolog教程1-补充教程(写在正式教程的前面)

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:16615 更新时间:2004-6-27 文章录入:admin ]
如果你是一位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 = wanfei
Y = xietinfen ;

X = zouhuimin
Y = zhangxueyou ;

X = xietinfen
Y = 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 = zhangxueyou
Y = xietinfen ;

X = xietinfen
Y = zhangxueyou ;

X = liudehua
Y = 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教程2-入门

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:9336 更新时间:2004-6-27 文章录入:admin ]

探索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
no

以上程序中的一些函数以后还会详细的介绍的。最后的那个no表示没有其他的人了。

进入下一章

从下一章起,就开始正式介绍Prolog的编程方法了。我将用一个实例来介绍Prolog,这是一个文字的冒险游戏,你所扮演的角色是一个三岁的小女孩,你想睡觉了,可是没有毛毯(nani)你就不能安心的睡觉。所以你必须在那个大房子中找到你的毛毯,这就是你的任务。这个游戏能够显示出一些Prolog的独到之处,不过Prolog的功能远不止编个简单的游戏,所以文中还将介绍一些其他的小程序。

Prolog教程3-事实

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:7175 更新时间:2004-6-27 文章录入:admin ]
事实 (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'

下面的由符号组成的也是合法的原子:

变量和原子相似, 但是开头字符四大写字母或是下划线。例如:

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谓词定义房间,一共有五条子句,它们都是事实,如图2.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教程4-简单查询

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:5630 更新时间:2004-6-27 文章录入:admin ]
现在我们的游戏中已经有了一些事实,使用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.

就可以开始调试你的程序了。

在下一章中将介绍更为复杂的查询。

Prolog教程5-混合查询

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:5119 更新时间:2004-6-27 文章录入:admin ]
我们可以把简单的查询连接起来,组成一些较复杂的查询。例如,如果我们想知道厨房里能吃的东西,就可以向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

除了使用逻辑的方法理解混合查询外,还可以通过分析程序的运行步骤来理解。用程序的语言来说就是“首先找到一样位于厨房的东西,然后判断它能否食用,如果不能,就到厨房里找下一样东西,再判断能否食用。一直如此重复,直到找到答案或把厨房的东西全部查完为止。”

请参照下图来理解。

调用查询后,程序将按照下面的步骤运行,请参照上图来理解。

搜索第一个目标,如果成功转到2,如果失败则回答'no',查询结束。
搜索第二个目标,如果成功转到3,如果失败转到1。
把绑定的变量的值输出。用户输入';'后转到2。
上面的例子中只有一个变量,下面我们再来看一个有两个变量的例子。

?- 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.
1 CALL location(X, kitchen)
1 EXIT (2) location(apple, kitchen)
2 CALL write(apple)
apple
2 EXIT write(apple)
3 CALL nl

3 EXIT nl
4 CALL fail
4 FAIL fail
3 REDO nl
3 FAIL nl
2 REDO write(apple)
2 FAIL write(apple)
1 REDO location(X, kitchen)
1 EXIT (6) location(broccoli, kitchen)
2 CALL write(broccoli)
broccoli
2 EXIT write(broccoli)
3 CALL nl

3 EXIT nl
4 CALL fail
4 FAIL fail
3 REDO nl
3 FAIL nl
2 REDO write(broccoli)
2 FAIL write(broccoli)
1 REDO location(X, kitchen)
1 EXIT (7) location(crackers, kitchen)
2 CALL write(crackers) crackers
2 EXIT write(crackers)
3 CALL nl

3 EXIT nl
4 CALL fail
4 FAIL fail
3 REDO nl
3 FAIL nl
2 REDO write(crackers)
2 FAIL write(crackers)
1 REDO location(X, kitchen)
1 FAIL location(X, kitchen)
no

下面请你分析一下,

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

的输出的结果是什么呢?

Prolog教程6-规则

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:4648 更新时间:2004-6-27 文章录入:admin ]
前面我们已经说过,谓词是使用一系列的子句来定义的。以前我们所学习的子句是事实,现在让我们来看看规则吧。规则的实质就是储存起来的查询。它的语法如下:

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).它代表玩家所在的位置。以后我们将学习如何改变此事实。现在来试是功能吧,

?- look.
You are in the kitchen
You can see:
apple
broccoli
crackers
You can go to:
office
cellar
dining
room
yes

好了到此,我们已经学会了Prolog的基本编程方法,下一章将总结一下,并再举几个例子,此后我们将进入较深的学习。

Prolog教程7-小结

[ 作者:佚名 转贴自:垂钓听竹轩http://cdtzx.swiki.net 点击数:4275 更新时间:2004-6-27 文章录入:admin ]
到现在为止,我们已经对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(c).

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教程8-算术

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:2983 更新时间:2004-8-21 文章录入:admin ]
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教程9-数据管理

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:3101 更新时间:2004-8-21 文章录入:admin ]
Prolog的程序就是谓词的数据库,我们通常把这些谓词的子句写入Prolog的程序中的。在运行Prolog时,解释器首先把所有的子句调入到内存中。所以这些写在程序中的子句都是固定不变的。那么有没有办法动态地控制内存中的子句呢?Prolog提供了这方面的功能。这就意味着,Prolog程序在运行过程中,还能够改变它自己。它使用一些内部谓词来完成这个功能。最重要的几个谓词如下:

asserta(X)
把子句X当作此子句的谓词的第一个子句加入到动态数据库中。它和I/O内部谓词的流程控制相同。回溯是失败,并且不会取消它所完成的工作。例如:如果内存中已经有了下面的几个事实:

people(a).
people(b).
people(c).

如果运行了asserta(people(d))之后,内存中的people/1的子句就变成了下面这个样子:

people(d).
people(a).
people(b).
people(c).

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子句能够共享信息。而asserts和retracts就是控制这些全局数据的工具。

使用全局数据有助于在子句之间快速的传递信息。不过,这种方式隐藏了子句之间的调用关系,所以一旦程序出错,是很难找到原因的。

我们完全也可以不使用assert和retract来完成上述的功能,不过这就需要把信息作为参数在子句中传递。在这种情况下,游戏中的状态将使用谓词的参数来储存,而不是谓词的子句。每一个谓词的入口参数是当前状态,而出口参数则为此谓词修改后的状态,状态在谓词之间传递,从而达到了预期的目的。我们还将在以后的章节中介绍这种方法。

我们现在所编写的程序并不都是从纯逻辑的考虑出发的,不过你可以看出使用Prolog编写这个游戏的过程非常自然,并没有什么晦涩难懂的东西。

一般情况下,asserta等谓词是不会在回溯的时候还原数据库的,所以上面的几个数据管理谓词的内部流程与I/O谓词相同,不过我们可以很容易的编写出能够在回溯时取消修改的谓词。

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

首先第一个子句被运行,在数据库中添加一条X子句。当其后的目标失败而产生回溯时,第二个子句将被调用,于是它把第一个子句的操作给取消了,又把子句X从数据库中上除了。

Prolog教程11-数据结构

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:3467 更新时间:2004-8-24 文章录入:admin ]
到目前为止,所介绍的事实、查询以及规则都使用的是最简单的数据结构。谓词的参数都是原子或者整数,这些都是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).

要注意变量的位置哟,不要搞混了。

Prolog教程11-联合

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:3299 更新时间:2004-8-29 文章录入:admin ]
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教程12-列表

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:5406 更新时间:2004-9-4 文章录入:admin ]
为了能够更好地表达一组数据,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教程13-操作符

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:5459 更新时间:2004-9-25 文章录入:admin ]
我们已经学习过了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.
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与其它语言的最大区别—-程序与数据的高度统一。正是它的这种极其简洁的表达形式,使得它被广泛地应用于人工智能领域。

Prolog教程14-截断

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:2239 更新时间:2004-8-21 文章录入:admin ]
直到目前为止,我们都一直在使用Prolog内建的回溯功能。使用此功能可以方便地写出结构紧凑的谓词来。

但是,并不是所有的回溯都是必须的,这时我们需要能够人工地控制回溯过程。Prolog提供了完成此功能的谓词,他叫做cut,使用符号!来表示。

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教程15-流程控制

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:2446 更新时间:2004-8-21 文章录入:admin ]
在前面的章节中,我们了解了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教程16-自然语言

[ 作者:佚名 转贴自:http://cdtzx.swiki.net/1 点击数:2768 更新时间:2004-8-21 文章录入:admin ]
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(R).
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(C),
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(C) :-
read_list(L),
command(CL,L),
C =.. CL, !.

get_command(_) :-
write('I don''t understand'), nl, fail.

到此为止,我们的Prolog教程就全部结束了,但是你的工作没有结束,如果想很好地掌握这门语言,还有很漫长的路要走。

非常感谢您阅读完这个教程,接下来就请进入实战部分吧。