Learn Prolog Now 翻译 - 第三章 - 递归 - 第二节,规则顺序,目标顺序,终止

内容提要

规则顺序

目标顺序

终止

Prolog是第一门比较成功的逻辑编程语言。逻辑编程语言内在实现是简单和富有魅力的:程序员的工作简单地说就是描述问题;程序员应该写下(使用语言的逻辑)声明性的规格说明

(即,一个知识库),去描述有趣的状态、事实和关系;程序员不应该告诉计算机如何去实现,而他根据问一些问题去获取信息,逻辑编程语言会给出答案。

然而,以上是理想情况,Prolog本身也确实通过一些重要的特征,往这个方向在努力。但是Prolog不是,重复一次,不是一门完整的逻辑编程语言。如果你只是从声明性方面去思考

Prolog程序,那么实际使用上去就会十分困难。正如我们之前章节学习到的,Prolog通过特有的方式得出查询的结果:它会自上而下地搜索知识库,从左到右地匹配每个子句的目标,并且

通过回溯从错误选择中进行恢复。这些程序性的方面对你的查询实际如何进行有很重要的影响。我们已经看过了一些例子在其声明性和程序性上不匹配(记得 p :- p吗?),接下来,我们

会继续看到,Prolog中很容易定义逻辑上相同的,但是实现上却十分不同的程序。让我们思考如下的情况。

请回忆之前我们定义的“后辈”程序,这里我们称为descend1.pl:

child(anne, bridget).
child(bridget, caroline).
child(caroline, donna).
child(donna, emily).

descend(X, Y) :- child(X, Y).
descend(X, Y) :- child(X, Z), descend(Z, Y).

这里我们做一个调整,并称新的程序为descend2.pl:

child(anne, bridget).
child(bridget, caroline).
child(caroline, donna).
child(donna, emily).

descend(X, Y) :- child(X, Z), descend(Z, Y).
descend(X, Y) :- child(X, Y).

这里的修改只是换了一些两个规则的顺序。所以如果只是从纯粹的逻辑定义上去理解,是什么都没有改变的。但是这种改变带给程序性上有什么不同吗?是的,但不明显。

比如,如果你查询所有的情况,将会看到descend1.pl的第一个回答是:

X = anne

Y = bridget

然而descend2.pl的第一个回答是:

X = anne

Y = emily

但是两个程序生成的答案是相同的,只是顺序不一致。这是具有共性的。简要地说,改变Prolog程序中规则的顺序,不会改变程序的行为。

我们继续,在descend2.pl的基础上,再进行一点小的修改,变成descend3.pl:

child(anne, bridget).
child(bridget, caroline).
child(caroline, donna).
child(donna, emily).

descend(X, Y) :- descend(Z, Y), child(X, Z).
descend(X, Y) :- child(X, Y).

请注意不同之处。这里我们对一个规则中的目标顺序进行了调整,而并非调整规则的顺序。现在,如果我们纯粹是从逻辑定义方面去理解,并没有任何不同,这和之前的两个定义

是一样的含义。但是这个程序的行为已经彻底改变。比如,如果我们进行查询:

?- descend(anne, emily).

Prolog会报错(类似“Out of local stack”)。Prolog进入了死循环,为什么?为了满足查询descend(anne, emily),Prolog会使用第一个规则。这意味着下一个目的是满足查询:

descend(W1, emily).

这里引入了新的变量W1。但是为了满足这个新目标,Prolog又会使用第一个规则,这意味着下一个目标会是:descend(W2, emily),这里引入了新的变量W2。当然,就会循环引入下

一个新的目标descend(W3, emily),接下去又是descend(W4, emily),等等。即,目标顺序的改变导致了程序的崩溃。使用标准的术语,这里我们有一个经典的关于左递归规则的例子,

即一个规则的主干部分最左端的目标是和规则的头部一样的。正如我们的例子所示,这种规则会导致非终止的计算。目标顺序,特别是左递归,当其不能终止时,就会变成一切罪恶之源。

还有,这里有一个针对规则顺序的提醒。我们之前提及规则顺序的改变,只会影响其查询的结果的顺序。但是这个结论在非终止程序中是不适用的。为了说明这点,请参考关于“后辈”

代码的第四次修改,称为descend4.pl:

child(anne, bridget).
child(bridget, caroline).
child(caroline, donna).
child(donna, emily).
descend(X, Y) :- child(X, Y).descend(X, Y) :- descend(Z, Y), child(X, Z).

这个程序只是在descend3.pl的基础上,调整了规则的顺序。现在这个程序和其他之前的程序具有一样的声明性含义,但是程序性上有所差别。首先,很明显的是,和descend1.pl和

descend2.pl有明显的差别,因为descend4.pl包含了左递归的规则,它会在进行一些查询时无法终止计算。比如,我们如果进行下面的查询,将无法终止计算:

?- descend(anne, emily).

但是descend4.pl在程序性上和descend3.pl也有所不同。规则顺序的不同导致了这种差异性。比如,descend3.pl在进行查询:

?- descend(anne, bridget).

时不会终止;但是descend4.pl在这个查询中会有结果。因为它会首先使用非递归的规则,并且找到答案,终止计算。所以在非终止的程序中,规则顺序的改变会导致找到一些额外的

解决方案。但无论如何,目标顺序的改变,而非规则顺序的改变,会使得程序性完全不同。为了确保计算能够终止,我们必须注意规则主干部分的目标顺序。因为调整规则的顺序,不会

改变非终止程序的本质——最多可以找到一些额外的解决方案而已。

总结一下,以上四个关于“后辈”程序的变种,描述了同样的问题,但是具体实现上有所不同。descend1.pl和descend2.pl在实现上的不同相对来说比较小:它们会生成相同的解决方案,

但是顺序不同。然而descend3.pl和descend4.pl在程序性上的差异和之前两个更大,因为它们的规则中目标的顺序不同。具体而言,这两个版本都包含了左递归规则,都会导致非终止的计算

行为。descend3.pl和descend4.pl在规则顺序上有所不同,意味着在某些情况下,descend4.pl可以终止计算,但是descend3.pl不能。

那么我们如何构建有用的Prolog程序呢?通常你首先需要通过声明性思考去确定整体的想法(蓝图),即思考如何精确地描述问题。这是解决问题的优先方式,同时也是逻辑编程的灵魂。

但是一旦你完成了这部分工作,就必须结合Prolog的具体实现检查你的方案。特别是需要检查规则中目标的顺序,从而确保计算能够终止。规则绝不要写出规则的主干最左边目标和规则头部

相同的情况,而是应该将触发递归的目标写到主干的最右边,即让递归目标出现在所有非递归目标的后面。这样做会使得Prolog有最多的机会不通过递归就找到答案。

时间: 2024-10-25 15:12:14

Learn Prolog Now 翻译 - 第三章 - 递归 - 第二节,规则顺序,目标顺序,终止的相关文章

Learn Prolog Now 翻译 - 第三章 - 递归 - 第一节,递归的定义

在Prolog中,谓词可以递归地定义.简要地讲,一个谓词是递归定义的,如果一个或者多个规则的定义中包含了规则自身. 例子1:消化 考虑如下的知识库: is_digesting(X, Y) :- just_ate(X, Y). is_digesting(X, Y) :- just_ate(X, Z), is_digesting(Z, Y). just_ate(mosquito, blood(john)). just_ate(frog, mosquito). just_ate(stork, frog

Learn Prolog Now 翻译 - 第三章 - 递归 - 第四节,更多的实践和练习

在学习了前三章内容后,我们应该对Prolog编程有了直观和理性的认识.由于合一.变量初始化.证明搜索和递归都是Prolog的核心概念,所以有如下更多的一些实践和练习. 这里我会先录入题目,后期再给出我自己的程序代码和一些思考. 实践1 试想有如下的描述迷宫的知识库.其中的事实描述了点和点之间的联通关系,即connected/2谓词逻辑给出了这样的事实:迷宫中能从参数1的点,直接到达参数2的点.而且, 联通关系是有方向的.单向不能往返的: connected(1,2). connected(3,4

Learn Prolog Now 翻译 - 第三章 - 递归 - 第三节,练习题和答案

练习题3.1 在之前的章节中,我们已经讨论了如下的谓词逻辑: descend(X, Y) :- child(X, Y). descend(X, Y) :- child(X, Z), descend(Z, Y). 假设我们将谓词逻辑重构如下: descend(X, Y) :- child(X, Y). descend(X, Y) :- descend(X, Z), descend(Z, Y). 这会导致问题吗? 我的答案: 1. 这个谓词逻辑是有问题的,因为规则2中存在左递归的情况,即规则2的主干

Learn Prolog Now 翻译 - 第四章 - 列表 - 第二节,列表成员

内容提要 本章主要介绍使用递归操纵列表的一个实际例子:判断一个元素是否在包含在一个列表中. 是时候介绍第一个Prolog中通过递归操纵列表的程序例子了.我们最感兴趣的事情之一是,某个对象是否是列表中的元素.所以,我们想写一个程序,当假设输入是一个对象X和一个列表L, 得出结果是X是否属于L.这个程序的名字通常是:member,是Prolog程序中使用递归操纵列表最简单的例子,如下: member(X, [X|T]). member(X, [H|T]) :- member(X, T). 这就是全部

Learn Prolog Now 翻译 - 第四章 - 列表 - 第一节,列表定义和使用

内容提要 列表定义: 合一在列表中的使用: 匿名变量: 列表定义 正如名字暗示的,列表就是多个元素组成的集合.更精确地说,是元素的有限序列.在Prolog中的列表,有如下的一些具体例子: [mia, vincent, jules, yolanda] [mia, robber(honey_bunny), X, 2, mia] [ ] [mia, [vincent, jules], [butch, girlfriend(butch)]] [[ ], dead(z), [2, [b, c]], [ ]

Learn Prolog Now 翻译 - 第五章 - 数字运算 - 第二节,数字运算与列表

内容提要 列表中的一些数字运算,累加器 尾递归调用 列表中的一些数字运算,累加器 关于数字运算最为重要的应用,可能是获取一些数据结构体的一些有用事实,比如列表.例如,知道列表的长度是很有用的.我们将会给出一些使用列表和数字运算的例子. 一个列表的长度是多少?这里有一个递归定义: 1. 空列表的长度为0. 2. 非空列表的长度为 1 + len(T),其中len(T)是非空列表的尾部. 这个定义在Prolog中很容易实现,以下是实现代码: len([], 0). len([_|T], N) :-

Learn Prolog Now 翻译 - 第四章 - 列表 - 第三节,递归遍历列表

内容提要 通过递归对列表进行遍历,从而完成各种操作. member/2这个谓词逻辑通过递归遍历了列表,对列表头部有一些操作,然后递归地对列表尾部做另外一些相同的操作.通过递归遍历列表在Prolog是十分普遍的做法, 事实上,我们必须要掌握这项技能.所以我们学习如下的例子. 当我们使用列表的时候,我们经常会将一个列表和另一个列表进行对比,或者拷贝一个列表的内容到另一个列表去,或者翻译一个列表到内容到另一个列表去,或者 类似到一些操作.这里有一个例子,假设我们有一个谓词a2b/2,有两个参数,第一个

Learn Prolog Now 翻译 - 第五章 - 数字运算 - 第三节,整数的比较

内容提要 Prolog中如何进行整数的比较 整数比较的实际应用 Prolog中如何进行整数的比较 一些Prolog的运算谓词可以实际地进行运算(即,不需要通过“is”协助),这些运算谓词都是进行整数比较的操作符. 运算实例 Prolog表达式 x < y   X < Y. x ≤ y  X =< Y. x = y  X =:= Y. x /= y X =\= Y. x ≥ y   X >= Y. x > y X > Y. 这些操作符有明确的含义,可以直接在Prolog中

Learn Prolog Now 翻译 - 第四章 - 列表 - 第四节,练习题和答案

练习题4.1 Prolog将会如何回答下面的查询? 1. [a, b, c, d] = [a, [b, c, d]]. 2. [a, b, c, d] = [a | [b, c, d]]. 3. [a, b, c, d] = [a, b, [c, d]]. 4. [a, b, c, d] = [a, b | [c, d]]. 5. [a, b, c, d] = [a, b, c, [d]]. 6. [a, b, c, d] = [a, b, c | [d]]. 7. [a, b, c, d] =