截断和否定

转载自:https://mp.weixin.qq.com/s/w5cjPDg0SDGiIMQjpXgYBw

本章有两个主要目标:

1.解释如何在cut谓词的帮助下控制Prolog的回溯行为。

2.解释如何将截断打包成更结构化的形式,即否定为失败。

 

 

1   截断

自动回溯是Prolog最具特色的功能之一。但是回溯会导致效率低下。有时,Prolog可能会浪费时间探索无能为力的可能性。对其行为的这一方面进行一些控制将是一件令人愉快的事情,但是到目前为止,我们仅看到了两种(相当粗糙的)方法:更改规则顺序和更改目标顺序。但是还有另一种方式。有一个内置的Prolog谓词! (感叹号),称为截断,它提供了一种更直接的方式来控制Prolog寻找解的方式。

究竟截断了什么,它有什么作用?这只是我们在编写子句时可以使用的特殊原子。例如,

 

p(X):- b(X), c(X), !, d(X), e(X).

 

是完美的Prolog规则。至于截断的目标,首先,这是一个永远成功的目标。其次,更重要的是,它具有副作用。假设某个目标使用此子句(我们将此目标称为父目标)。然后,截断将Prolog提交给自父目标与规则左侧合一以来所做的任何选择(重要的是,包括使用该特定子句的选择)。让我们看一个例子,看看这意味着什么。

首先考虑下面的免截断代码:

 

p(X):- a(X).

 

p(X):- b(X), c(X), d(X), e(X).

 

p(X):- f(X).

 

a(1). b(1). c(1). d(2). e(2). f(3).

        b(2). c(2).

 

如果我们提出查询p(X),我们将得到以下响应:

 

X = 1;

 

X = 2;

 

X = 3.

 

这是Prolog如何找到这三种解答的搜索树。注意它必须回溯一次,即当它进入p / 1的第二子句并决定用b(1)而不是b(2)合一第一个目标时。

但是现在假设我们在第二个子句中插入了一个cut:

 

p(X):- b(X), c(X), !, d(X), e(X).

 

如果现在构成查询p(X),我们将得到以下响应:

 

X = 1.

false.

 

这里发生了什么?让我们考虑一下。

 

1.  p(X)首先与第一条规则合一,因此我们得到一个新的目标a(X)。通过将X实例化为1,Prolog将a(X)与事实a(1)合一在一起,我们找到了一个解答。到目前为止,这正是程序的第一个版本中发生的。

2.  然后,我们继续寻找第二个解答。 p(X)与第二条规则合一,因此我们得到了新的目标b(X),c(X),!,d(X),e(X)。通过将X实例化为1,Prolog合一了b(X )的事实b(1),因此我们现在有了目标c(1),!,d(1),e(1)。但是c(1)在数据库中,因此可以简化为!,d(1),e(1)。

3.  现在进行重大更改。 !目标是成功的(一如既往),并致力于使我们做出迄今为止的选择。特别地,我们致力于使X = 1,并且我们也致力于使用第二条规则。

4.  但是d(1)失败。而且,我们无法重新满足目标p(X)。当然,如果允许我们尝试X = 2的值,则可以使用第二条规则来生成解(这是该程序的原始版本中发生的事情)。但是我们无法做到这一点:截断已从搜索树中消除了这种可能性。并且可以肯定的是,如果允许我们尝试第三条规则,我们可以生成解X = 3。但是我们不能这样做:再次,截断已从搜索树中消除了这种可能性。

 

如果您查看搜索树,您会发现所有内容都归结为以下几点:当目标d(1)没有引向可替代选择的任何节点时,搜索就会停止。搜索树中的十字表示截断的分支。

有一点值得强调:截断仅使我们做出选择,因为父目标与包含截断的子句的左侧合一了。例如,按照以下形式

 

q:- p1,...,pn, !, r1,…,rm

 

当我们达到截断点时,它使我们不得不对q使用此特定子句,并且使我们承担求值p1,…,pn时所做的选择。但是,我们可以自由地在r1,...,rm之间回溯,我们也可以自由地在达到目标q之前做出的选择中回溯。一个具体的例子将使这一点变得清楚。

 

首先考虑以下免截断程序:

 

s(X,Y):- q(X,Y).

s(0,0).

 

q(X,Y):- i(X), j(Y).

 

i(1).

i(2).

 

j(1).

j(2).

j(3).

 

它的行为如下:

 

?- s(X,Y).

 

X = 1

Y = 1 ;

 

X = 1

Y = 2 ;

 

X = 1

Y = 3 ;

 

X = 2

Y = 1 ;

 

X = 2

Y = 2 ;

 

X = 2

Y = 3 ;

 

X = 0

Y = 0;

false.

 

这是相应的搜索树:

假设我们向定义q / 2的子句添加了一个cut:

 

q(X,Y):- i(X), !, j(Y).

 

现在该程序的行为如下:

 

?- s(X,Y).

 

X = Y, Y = 1 ;

 

X = 1

Y = 2 ;

 

X = 1

Y = 3 ;

 

X = Y, Y = 0;

 

让我们看看为什么。

 

1.  s(X,Y)首先与第一条规则合一,这为我们提供了一个新的目标q(X,Y)。

2.  然后将q(X,Y)与第三条规则合一,因此我们得到了新的目标i(X), !, j(Y)。通过将X实例化为1,Prolog将i(X)与事实i(1)合一。这使我们有了目标!,j(Y)。当然,截断是成功的,并使我们致力于迄今为止所做的选择。

3.  但是这些选择是什么?这些:X = 1,并且我们正在使用此子句。但请注意:我们尚未选择Y的值。

4. 然后,Prolog继续,通过将Y实例化为1,Prolog将j(Y)与事实j(1)合一。因此,我们找到了解答。

5. 但是我们可以找到更多。 Prolog可以自由尝试Y的另一个值,因此它回溯并将Y设置为2,从而找到了第二个解答。实际上,它可以找到另一个解答:再次回溯时,它将Y设置为3,从而找到了第三个解答。

6.  但是这些都是j(X)的替代方案。不允许在截断的左侧进行回溯,因此无法将X重置为2,因此无法找到免截断程序找到的后面三个解答。但是,允许在q(X,Y)之前达到的目标回溯,以便Prolog可以找到s / 2的第二个子句。

 

这是相应的搜索树:

2  使用截断

好了,我们现在知道什么是截断。但是我们如何在实践中使用它,为什么它如此有用呢?作为第一个示例,让我们定义一个(免截断)谓词max / 3,该谓词将整数作为参数,如果第三个参数是前两个的最大值,则该谓词成功。例如,查询

 

?- max(2,3,3).

 

 

?-max(3,2,3).

 

 

?-max(3,3,3).

 

应该成功,并且查询

 

?- max(2,3,2).

 

 

?- max(2,3,5).

 

 

应该失败。当然,我们也希望程序在第三个参数为变量时运行。也就是说,我们希望程序能够为我们找到前两个参数的最大值:

 

?- max(2,3,Max).

 

Max = 3

 

?-max(2,1,Max).

 

Max = 2

 

现在,很容易编写一个执行此操作的程序。这是第一次尝试:

 

max(X,Y,Y):- X=<Y.

max(X,Y,X):- X>Y.

 

这是一个完全正确的程序,我们可能会很想在这里停下来。但是我们不应该:这还不够好。

有什么问题?存在潜在的低效率。假设此定义用作较大程序的一部分,并且在调用max(3,4,Y)的某个位置。程序将正确设置Y = 4,但是现在考虑如果在某个阶段强制回溯会发生什么情况,程序将尝试使用第二个子句重新满足max(3,4,Y),这完全没有意义:最大3和4中的是4,就是这样。没有第二个解答可以找到。换句话说,上述程序中的两个子句是互斥的:如果第一个子句成功,则第二个子句必须失败,反之亦然。因此,尝试重新满足此条款是完全浪费时间。

借助截断,这很容易修复。我们需要坚持要求Prolog永远不要尝试这两个子句,而以下代码可以做到这一点:

 

max(X,Y,Y) :- X =< Y,!.

max(X,Y,X) :- X>Y.

 

注意这是如何工作的。如果调用max(X,Y,Y)并且X = <Y成功,则Prolog将到达截断。在这种情况下,第二个参数是最大值,仅此而已。另一方面,如果X = <Y失败,则Prolog转到第二个子句。

请注意,此截断不会更改程序的含义。我们的新代码可以提供与旧代码完全相同的答案,但是效率更高。实际上,该程序与以前的版本完全相同,除了截断,这是一个很好的信号,表明截断是明智的选择。像这样的截断(不会改变程序的含义)具有特殊的名称:它们称为“绿色截断”。

但是有些读者会不喜欢这段代码。毕竟,第二行不是多余的吗?如果必须使用此行,我们已经知道第一个参数大于第二个参数。我们不能借助新的cut结构来提高效率吗?试试吧。这是第一次(错误的)尝试:

 

max(X,Y,Y) :- X =< Y,!.

max(X,Y,X).

 

请注意,这与我们先前的绿色截断max / 3相同,除了我们在第二个子句中取消了>测试。有多好好吧,对于某些查询来说还可以。特别是,当我们提出第三个参数是变量的查询时,它可以正确回答。例如:

?- max(100,101,X).

X = 101.

 

 

?- max(3,2,X).

X = 3.

 

但是,它与绿色截断程序不同:新的max / 3无法正常运行。考虑所有三个参数都实例化时会发生什么。例如,考虑查询

 

?- max(2,3,2).

 

显然,此查询应该失败。但是在我们的新版本中,它将成功!为什么?嗯,此查询根本不会与第一个子句的开头合一,因此Prolog直接进入第二个子句。并且查询将与第二个子句合一,并且(通常)查询成功!因此,也许摆脱该测试毕竟不是那么明智。

但是还有另一种方式。新代码的问题很简单,就是我们在遍历截断之前进行了变量合一,假设我们以更智能的方式处理变量(使用三个变量而不是两个),并在跨过剪切后显式合一:

 

max(X,Y,Z):- X =< Y, !, Y = Z.

max(X,Y,X).

 

正如读者应该检查的那样,该程序确实可以运行,并且(如我们希望的那样)它避免了在我们的绿色版本max / 3的第二个子句中进行显式比较。

但是该程序的新版本与绿色截断版本之间存在重要区别。新程序中的截断是所谓的红色截断的经典示例。正如该项所暗示的那样,此类截断具有潜在的危险。为什么?因为如果进行这样的截断,我们将无法获得同等的程序。也就是说,如果我们删除截断,则生成的代码将不再计算两个数字的最大值。换句话说,截断的存在对于程序的正确运行是必不可少的。 (绿色剪切版本不是这种情况—那里的截断只是提高了效率。)因为红色截断是必不可少的截断,所以它们的存在表示包含它们的程序不是完全声明性的。现在,在某些情况下进行红色截断可能会有用,但请注意!它们的使用可能导致细微的编程错误,并使代码难以调试。

那么该怎么办?最好按照以下方式工作。尝试获得良好,清晰,无截断的程序,然后尝试使用截断来提高其效率。尽可能使用绿色截断。仅当绝对必要时才应使用红色截断,这是一个好主意,明确注释代码中的任何红色截断。以这种方式工作将最大程度地在声明性清晰度和过程效率之间取得良好平衡。

 

否定失败

Prolog最有用的功能之一就是它使我们能够概括的简单方法。要说vincent喜欢汉堡,我们只写:

 

enjoys(vincent, X):- burger(X).

 

但是在现实生活中,规则有例外。也许Vincent不喜欢Big Kahuna汉堡。也就是说,也许正确的规则确实是:Vincent喜欢汉堡,除了Big Kahuna汉堡。精细。但是我们如何在Prolog中说明呢?

首先,让我们介绍另一个内置谓词:fail / 0。顾名思义,fail / 0是一个特殊符号,当Prolog遇到目标时,它将立即失败。听起来可能没什么用,但是请记住:当Prolog失败时,它会尝试回溯。因此,fail / 0可被视为强制回溯的指令。当与cut结合使用时(阻止回溯),fail / 0使我们能够编写一些有趣的程序,特别是,它使我们可以定义常规规则的例外。

考虑以下代码:

 

enjoys(vincent,X) :- big_kahuna_burger(X),!,fail.

enjoys(vincent,X) :- burger(X).

 

burger(X) :- big_mac(X).

burger(X) :- big_kahuna_burger(X).

burger(X) :- whopper(X).

 

big_mac(a).

big_kahuna_burger(b).

big_mac(c).

whopper(d).

 

前两行描述了Vincent的偏好。最后六行描述了一个包含四个汉堡的世界,分别是a,b,c和d。我们还会提供有关它们是哪种汉堡的信息。考虑到前两行确实描述了Vincent的偏好(也就是说,他喜欢除Big Kahuna汉堡之外的所有汉堡),那么他应该喜欢a,c和d汉堡,而不是b。确实,这是发生了什么:

 

?- enjoys(vincent,a).

true.

 

?- enjoys(vincent,b).

false.

 

?- enjoys(vincent,c).

true.

 

?- enjoys(vincent,d).

true.

 

这是如何运作的?关键是结合并在第一行中显示!和fail/0(甚至有一个名字:它称为cut-fail组合)。当我们对查询enjoys(vincent,b)进行设置时,将应用第一条规则,然后我们便会到达截断。这使我们能够做出选择,尤其是阻止访问第二条规则。但是随后我们fail/ 0。这试图强制回溯,但是截断阻止了它,因此我们的查询失败。

这很有趣,但是并不理想。首先,请注意规则的顺序至关重要:如果颠倒前两行,我们将无法获得想要的行为。同样,截断是至关重要的:如果将其删除,程序将不会以相同的方式运行(因此这是红色截断)。简而言之,我们有两个相互依存的子句,它们固有地使用了Prolog的程序特性。显然有用的事情在这里进行着,但是如果我们可以提取出有用的部分并以更健壮的方式打包它会更好。

而且我们可以。关键的观察是,第一句实质上是一种说法,如果X是Big Kahuna汉堡,则文森特不会喜欢X。也就是说,不合格组合似乎为我们提供了某种形式的否定。确实,这是至关重要的概括:过失组合可以让我们定义一种否定形式,称为否定。这是如何做:

 

neg(Goal):- Goal,!,fail.

neg(Goal).

 

对于任何Prolog目标,如果目标未成功,则neg(Goal)将成功。

使用新的neg/1谓词,我们可以更清晰地描述Vincent的偏好:

 

 

enjoys(vincent,X) :- burger(X),

  neg(big_kahuna_burger(X)).

 

也就是说,如果X是汉堡而X不是Big Kahuna汉堡,则Vincent会喜欢X。这与我们最初的说法非常接近:Vincent喜欢汉堡,除了Big Kahuna汉堡。

否定失败是重要的工具。它不仅提供有用的表达能力(特别是描述异常的能力),而且还以相对安全的形式提供它。通过将否定视为失败(而不是使用较低级别的cut-fail组合),我们有更好的机会来避免经常伴随使用red cut的编程错误。实际上,否定即失败是如此有用,以至于它作为标准Prolog的一部分内置,因此我们根本不必对其进行定义。在标准的Prolog中,运算符 \+ 表示否定表示失败,因此我们可以按以下方式定义Vincent的首选项:

 

enjoys(vincent,X) :- burger(X),

\+ big_kahuna_burger(X).

 

尽管如此,还是有几个警告的措辞是正确的:不要误以为否定就是失败,就像逻辑否定一样起作用。没错再次考虑我们的汉堡世界:

 

burger(X) :- big_mac(X).

burger(X) :- big_kahuna_burger(X).

burger(X) :- whopper(X).

big_mac(a).

big_kahuna_burger(b).

big_mac(c).

whopper(d).

 

如果我们将查询设为enjoys(vincent,X),我们将获得正确的响应序列:

 

X = a;

 

X = c;

 

X = d.

 

但是现在假设我们将第一行重写如下:

 

enjoys(vincent,X) :- \+ big_kahuna_burger(X), burger(X).

 

请注意,从声明的角度来看,这应该没有什么区别:毕竟,burger(x)而不是big_kahuna_burger(x)在逻辑上等效于big_kahuna_burger(x)和burger(x)。也就是说,无论变量x表示什么,这些表达式之一不可能为true,而另一个为false。尽管如此,当我们提出相同的查询时,会发生以下情况:

 

?- enjoys(vincent,X).

false.

 

这是怎么回事?好吧,在修改后的数据库中,Prolog首先要检查的是 \+ big_kahuna_burger(X)是否成立,这意味着它必须检查big_kahuna_burger(X)是否失败。但这成功。毕竟,数据库包含信息big_kahuna_burger(b)。因此,查询\ + big_kahuna_burger(X)失败,因此原始查询也是如此。简而言之,这两个程序之间的关键区别在于,在原始版本(运行正常的版本)中,我们仅在实例化变量X之后才使用\ +。在新版本(出现问题的版本)中,我们使用\ +在我们这样做之前。差异至关重要。

综上所述,我们发现将否定作为失败不是逻辑上的否定,它具有必须被理解的过程维度,尽管如此,它还是一个重要的编程结构:尝试将否定作为失败比使用否定通常是更好的主意。编写包含大量使用红色截断的代码。但是,“一般”并不意味着“总是”。在某些情况下,最好使用红色截断。

例如,假设我们需要编写代码来捕获以下条件:如果a和b成立,或者如果a不成立,而c还成立,则p成立。可以在否定的帮助下非常直接地将其捕获为失败:

 

p :- a, b.

 

p :- \+ a, c.

 

但是假设a是一个非常复杂的目标,这个目标需要大量时间才能计算出来。以这种方式进行编程意味着我们可能必须计算两次,这可能意味着我们的性能降低得令人无法接受。如果是这样,最好使用以下程序:

 

p :- a,!,b.

p :- c.

 

请注意,这是一个捷径:删除它会更改程序的含义。

总而言之,没有通用的准则可以涵盖您可能遇到的所有情况。编程既是一门艺术,也是一门科学:这就是使它如此有趣的原因。您需要尽可能多地了解所使用的语言(无论是Prolog,Java,Perl或其他语言),了解您要解决的问题,并知道什么是可以接受的解决方案,然后:继续努力!

 

4  练习

 

练习10.1  假设我们有以下数据库:

 

p(1).

p(2):-!.

p(3).

 

将Prolog的所有答案写在以下查询中:

 

?- p(X).

?- p(X),p(Y).

?- p(X),!,p(Y).

 

练习10.2  首先,说明以下程序的作用:

 

class(Number,positive) :- Number > 0.

class(0,zero).

class(Number,negative) :- Number < 0.

其次,通过添加绿色截断来改进它。

 

练习10.3  在不使用cut的情况下,编写谓词split / 3,它将整数表分成两个表:一个包含正数(和零),另一个包含负数。例如:

 

split([3,4,-5,-1,0,4,-9],P,N)

 

应该返回:

 

P = [3,4,0,4]

 

N = [-5,-1,-9].

 

然后在截断的帮助下改进该程序,而无需更改其含义。

 

练习10.4    回想一下,在练习3.3中,我们为您提供了以下知识库:

 

directTrain(saarbruecken,dudweiler).

directTrain(forbach,saarbruecken).

directTrain(freyming,forbach).

directTrain(stAvold,freyming).

directTrain(fahlquemont,stAvold).

directTrain(metz,fahlquemont).

directTrain(nancy,metz).

 

我们要求您编写一个递归谓词travelFromTo/2,它告诉我们何时可以乘火车在两个镇之间旅行。

现在,可以假设只要有可能从A到B乘坐直达火车,也有可能从B到A乘坐直达火车。请将此信息添加到数据库中。然后编写谓词route/3,其中列出了通过搭乘火车从一个城镇到另一个城镇而要逗留的城镇表。例如:

 

?- route(forbach,metz,Route).

Route = [forbach,freyming,stAvold,fahlquemont,metz]

 

练习10.5   回顾第一章中对嫉妒的定义。

 

jealous(X,Y):- loves(X,Z), loves(Y,Z).

 

在Vincent和Marsellus都爱Mia的世界中,Vincent会嫉妒Marsellus和Vincent会被 Marsellus妒忌。但是Marsellus也会嫉妒自己,Vincent也会嫉妒自己。修改Prolog对嫉妒的定义,以使人们不会嫉妒自己。

 

5   实践环节

 

本课程的目的是帮助您熟悉作为失败的截断和否定。首先是一些键盘练习:

 

1.试用文本中定义的max / 3谓词的所有三个版本:免截断版本,绿色截断版本和红色截断版本。与往常一样,“尝试”意味着“运行跟踪”,并且应确保跟踪将所有三个参数都实例化为整数的查询以及将第三个参数作为变量给出的查询。

 

2.好,该吃汉堡了。试用文中讨论的所有方法来应对Vincent的偏好。也就是说,请尝试使用截断失败组合的程序,正确使用否定作为失败的程序,以及在错误的地方使用否定而弄糟它的程序。

 

现在进行一些编程:

 

1.定义一个谓词nu / 2(“ 不合一”),该谓词以两个项作为参数,如果两个词不合一则成功。例如:

 

nu(foo,foo).

false.

 

nu (foo,blob).

true.

 

nu(foo,X).

false.

 

您应该以三种不同的方式定义此谓词:

(a)首先(也是最简单的)借助=和\+编写它。

(b)其次,在=的帮助下编写它,但不要使用\+。

(c)第三,使用截断-失败组合书写。不要使用=,也不要使用\ +。

 

2.定义谓词unifiable(List1,Term,List2),其中List2是与Term合一的List1的所有成员的表。 List2的元素不应通过合一实例化。

例如

 

unifiable([X,b,t(Y)],t(a),List]

 

应该产生

 

List = [X,t(Y)].

 

请注意,X和Y仍未实例化。因此,棘手的部分是:如何在不实例化它们的情况下检查它们是否与t(a)合一?

(提示:考虑使用\+ term1 = term2形式的测试。为什么?考虑一下。您可能还想考虑\+ \+ term1 = term2形式的测试。)

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值