我试图理解为什么Prolog实现不会根据教科书中的执行模型行事 - 例如,Sterling和Shapiro的书中的“Prolog的艺术”(第6章,“纯粹的Prolog”,第6.1节“Prolog的执行模型”)。
我所指的执行模式是这个(Sterling& Shapiro的第93页):
输入:目标G和程序P
输出: G的一个实例,它是P的逻辑结果,否则不是
算法:
Initialize resolvent to the goal G
while resolvent not empty:
choose goal A from resolvent
choose renamed clause A' <- B_1, ..., B_n from P
such that A, A' unify with mgu θ
(if no such goal and clause exist, exit the "while" loop)
replace A by B_1, ..., B_n in resolvent
apply θ to resolvent and to G
If resolvent empty, then output G, else output NO
此外(同一本书的第120页),Prolog按从左到右的顺序选择目标(choose goal A
),并按照它们在程序中显示的顺序搜索子句(choose renamed clause ...
)
以下程序的定义为not
(程序中称为n
)和一个事实。
n(X) :- X, !, fail.
n(X).
f(a).
如果我试图证明n(n(f(X)))
,它会成功(根据两本教科书以及SWI Prolog,GNU Prolog和Yap)。但这不是有点奇怪吗?根据几本书所揭示的执行模型,这就是我期望发生的事情(跳过重命名变量以保持简单,因为无论如何都不会发生冲突):
解决方案:n(n(f(Z)))
统一将第一个子句中的X
与n(f(Z))
匹配,并将该目标替换为该子句的尾部。
解决方案:n(f(Z)), !, fail
。
统一在X
的第一个子句中再次匹配f(Z)
,并使用子句的尾部替换解决方案中的第一个目标
解决方案:f(Z), !, fail, !, fail
。
统一匹配f(Z)
- &gt;成功!现在这已从解决方案中消除。
解决方案:!, fail, !, fail
。
“!, fail, !, fail
”不应该成功!切割后有一个失败。故事结局。 (事实上,在我有权访问的所有Prolog系统中,输入!,fail,!,fail
作为查询将失败。
那么我可以假设教科书中的执行模型不正是Prolog使用的吗?
编辑:将第一个子句更改为n(X) :- call(X), !, fail
对我尝试的所有Prolog没有任何影响。
答案 0 :(得分:6)
下面的标题会告诉您这个特定算法的含义:
图4.2 逻辑程序的抽象解释器
此外,其描述如下:
输出: G 的实例,它是 P 的逻辑结果,否则为无。
也就是说,4.2中的算法仅向您展示如何计算逻辑程序的逻辑结果。它只会让您了解Prolog的实际工作方式。特别是无法解释!
。此外,4.2中的算法只能解释如何找到一个解决方案(“结果”),但Prolog试图以系统的方式找到所有这些解决方案,称为按时间顺序回溯。切割以非常特殊的方式干扰了按时间顺序的回溯,这在该算法的层面上无法解释。
您写道:
此外(同一本书的第120页),Prolog以从左到右的顺序选择目标(choose goal A)
,并按照它们在程序中显示的顺序搜索子句(choose renamed clause ...)
。
错过了一个重要的观点,你可以在第120页阅读:
Prolog的执行机制是从抽象解释器中通过选择最左边的目标获得的......并通过顺序搜索一个可统一的子句和回溯来替换子句的非确定性选择。
所以这是一个小小的补充“和回溯”,这使事情变得更加复杂。你无法在抽象算法中看到这一点。
这是一个很小的例子,表明在算法中没有明确处理回溯。
p :-
q(X),
r(X).
q(1).
q(2).
r(2).
我们将从p
开始,将其重写为q(X), r(X)
(没有其他方法可以继续)。
然后,选择q(X)
,并且θ= {X
= 1}。所以我们有r(1)
作为解决方案。但是现在,我们没有任何匹配子句,所以我们“退出while循环”并回答 no 。
但是等等,有一个解决方案!那么我们如何得到它呢?当选择q(X)
时,θ还有另一种选择,即θ= {X
= 2}。算法本身并未明确执行此操作的机制。它只说:如果你到处做出正确的选择,你会找到答案。为了从抽象算法中获得真正的算法,我们需要一些机制来实现这一点。
答案 1 :(得分:5)
您的程序不是纯粹的Prolog程序,因为它包含n / 1中的!/ 0。您可能会问自己一个更简单的问题:使用您的定义,为什么查询?- n(f(X)).
失败尽管您的程序中明显存在n(X)事实,这意味着n(X)是对于每个 X都是如此,因此特别适用于f(X)?这是因为由于使用了!/ 0而无法再单独考虑程序的子句,并且不能使用纯Prolog的执行模型。对于这种不纯的谓词,更现代和纯粹的替代方法通常是约束,例如dif / 2,您可以使用它来约束变量以区别于术语。
答案 2 :(得分:4)
当你到达最后一步时:
cut 裁剪在这里没有任何意义,第一个!
这里意味着“擦除一切”。因此解决方案变得空洞。 (这当然是假装,但足够接近)。fail
说要翻转决定,第二个fail
要翻转它。现在解决方案是空的 - 决定是“是”,并且仍然如此,两次翻转。 (这也是假装 ......“翻转”只有在回溯的情况下才有意义。)
你当然不能在解决方案的目标列表上放置一个!
,因为它不仅仅是要实现的目标之一。它具有操作含义,它通常表示“停止尝试其他选择”但是这个解释器保持没有跟踪任何选择(它“好像“立刻做出所有选择”。 fail
不仅仅是实现目标,它还说“你成功的地方说你没有,反之亦然”。
那么我可以假设教科书中的执行模型不正是Prolog使用的吗?
是的,当然,真正的Prologs有cut
和fail
,与您提到的抽象解释器不同。该解释器没有明确的回溯,而是通过魔术获得了多次成功(其选择本质上是 非确定性 ,就好像所有选择都是在一次,并行 - 真正的Prolog只有模拟通过顺序执行和明确的回溯, cut
所引用的 - 否则就没有意义了。
答案 3 :(得分:2)
您的测试目标中有一个额外的嵌套级别:
n(n(f(X))
而不是:
n(f(X))
事实上,如果我们尝试这样做,它会按预期工作:
$ prolog
GNU Prolog 1.3.0
By Daniel Diaz
Copyright (C) 1999-2007 Daniel Diaz
| ?- [user].
compiling user for byte code...
n(X) :- call(X), !, fail.
n(_X).
f(a).
user compiled, 4 lines read - 484 bytes written, 30441 ms
yes
| ?- f(a).
yes
| ?- n(f(a)).
no
| ?- n(f(42)).
yes
| ?- n(n(f(X))).
yes
| ?- n(f(X)).
no
| ?- halt.
所以你对Prolog的理解是正确的,你的测试用例不是!
<强>更新强>
显示否定否定的影响:
$ prolog
GNU Prolog 1.3.0
By Daniel Diaz
Copyright (C) 1999-2007 Daniel Diaz
| ?- [user].
compiling user for byte code...
n(X) :- format( "Resolving n/1 with ~q\n", [X] ), call(X), !, fail.
n(_X).
f(a) :- format( "Resolving f(a)\n", [] ).
user compiled, 4 lines read - 2504 bytes written, 42137 ms
(4 ms) yes
| ?- n(f(a)).
Resolving n/1 with f(a)
Resolving f(a)
no
| ?- n(n(f(a))).
Resolving n/1 with n(f(a))
Resolving n/1 with f(a)
Resolving f(a)
yes
| ?- n(n(n(f(a)))).
Resolving n/1 with n(n(f(a)))
Resolving n/1 with n(f(a))
Resolving n/1 with f(a)
Resolving f(a)
no
| ?- n(n(n(n(f(a))))).
Resolving n/1 with n(n(n(f(a))))
Resolving n/1 with n(n(f(a)))
Resolving n/1 with n(f(a))
Resolving n/1 with f(a)
Resolving f(a)
yes
| ?- halt.
答案 4 :(得分:2)
我认为你几乎是对的。问题出在这里:
RESOLVENT: !, fail, !, fail.
第一个!和失败来自第一个子句匹配的第二时间。另外两个来自第一个时间。
RESOLVENT: ![2], fail[2], ![1], fail[1].
剪切和失败对正在处理的子句有影响 - 而不是“调用”它的子句。如果您再次完成这些步骤,但使用这些注释,您将获得正确的结果。
![2], fail[2]
第二次调用n
失败而没有回溯。但其他调用(第一个)仍然可以回溯 - 它会:
RESOLVENT: n(_)
结果是“是”。
这表明Prolog使用堆栈规则保留有关回溯的信息。您可能对用作Prolog实现的模型的虚拟机感兴趣。它比您提到的执行模型复杂得多,但将Prolog转换为VM将使您更准确地了解Prolog的工作原理。这是沃伦抽象机(WAM)。 tutorial by Hasan Aït-Kaci是你能找到的最好的解释(它解释了切割,如果我没记错,原始的WAM描述中没有)。如果您不习惯抽象理论文本,您可以先尝试阅读Peter van Roy的文本:“1983-1993: the wonder years of sequential Prolog implementation”。这篇文章很清楚,基本上讲述了Prolog实现的历史,但是特别关注了WAM。但是,它没有显示切割是如何实现的。但是,如果您仔细阅读它,您可以选择Hasan的教程并阅读他实施剪辑的部分。
答案 5 :(得分:1)
虽然mat是正确的,因为你的程序不是纯粹的prolog(这是相关的,因为本章的标题是Pure Prolog),不仅因为你使用剪切,而且因为你编写处理其他谓词的谓词(纯prolog是一阶逻辑的子集)这不是主要问题;你只是缺少回溯
虽然你确实有一个削减,但是在目标n(f(X))成功之前不会达到。但是,如您所知,这将失败,因此prolog将回溯并匹配第二个条款。
我没有看到这与6.1中描述的模型相矛盾(并且很难相信其他书籍会描述一个模型,在失败后执行会继续执行,从而允许削减其他解决方案)。无论如何,我发现跳到结论&#34; Prolog实现根据教科书中的执行模型没有表现出来#34;非常类似于&#34;编译器存在一个错误&#34;,特别是因为&#34;反例&#34;表现得应该(不是(不是(真))应该是真的)