7

我试图理解为什么 Prolog 实现不按照教科书中的执行模型运行——例如,Sterling 和 Shapiro 的“Prolog 的艺术”书中的那个(第 6 章,“Pure 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 页),Prologchoose goal A以从左到右的顺序选择目标 ( ),并choose renamed clause ...按照它们在程序中出现的顺序搜索子句 ( )。

下面的程序有一个定义notn在程序中调用)和一个单一的事实。

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)-> 成功!现在,这已从解决方案中消除。

  • 解决方案:!, fail, !, fail

而“ !, fail, !, fail”应该不会成功!切割后有一个失败。故事结局。(事实上​​,!,fail,!,fail在我有权访问的所有 Prolog 系统中,作为查询输入都会失败)。

那么我可以假设教科书中的执行模型并不是Prolog使用的吗?

编辑:将第一个子句更改为对n(X) :- call(X), !, fail我尝试过的所有 Prolog 都没有影响。

4

6 回答 6

6

您的程序不是纯 Prolog 程序,因为它在 n/1 中包含一个 !/0。您可能会问自己一个更简单的问题:根据您的定义,为什么查询?- n(f(X)). 会失败,尽管您的程序中有一个事实 n(X),这意味着 n(X) 对于每个X 都是正确的,因此应该特别适用于f(X) 也是?这是因为使用了!/0,不能再孤立地考虑程序的子句,不能使用纯Prolog的执行模型。这种不纯谓词的更现代和更纯粹的替代方法通常是约束,例如 dif/2,您可以使用它来约束变量与术语不同。

于 2012-07-23T14:28:34.380 回答
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}。算法本身并没有明确说明执行此操作的机制。它只是说:如果你在任何地方做出正确的选择,你就会找到答案。为了从那个抽象的算法中得到一个真正的算法,我们因此需要一些机制来做到这一点。

于 2012-07-23T22:26:20.380 回答
4

当您到达最后一步时:

  • 解决:!,失败,!,失败

这里的cut!意思是“抹去一切”。所以解析器变成空的。(这当然是伪造的,但已经足够接近了)。削减在这里根本没有任何意义,第一个fail说翻转决定,第二个说fail把它翻转回来。现在解决方案是空的 - 决定是“是”,并且仍然如此,两次翻转。(这也是伪造的......“翻转”仅在存在回溯的情况下才有意义)。

您当然不能!在解决方案中的目标列表上进行削减,因为它不仅仅是要实现的目标之一。它具有操作意义,通常表示“停止尝试其他选择”,但该解释器跟踪任何选择(它“好像”一次做出所有选择)。fail也不仅仅是一个要实现的目标,它说“你成功的地方说你没有,反之亦然”。

那么我可以假设教科书中的执行模型并不是Prolog使用的吗?

是的,当然,真正的 Prolog 具有cut并且fail不同于您提到的抽象解释器。该解释器没有显式回溯,而是通过魔术获得了多次成功(它的选择本质上是不确定的,就好像所有选择都是同时并行做出的一样 - 真正的 Prolog 仅通过显式回溯的顺序执行来模拟一点,cut即引用 - 否则它根本没有任何意义)。

于 2012-07-23T20:44:49.767 回答
2

我认为你几乎是对的。问题在这里:

RESOLVENT: !, fail, !, fail.

首先 !和失败来自第二次匹配第一个子句。另外两个是一次。

RESOLVENT: ![2], fail[2], ![1], fail[1].

剪切和失败对正在处理的子句产生影响 - 而不是对“调用”它的子句产生影响。如果您再次完成这些步骤,但使用这些注释,您将获得正确的结果。

![2], fail[2]使第二次调用n失败而不回溯。但是另一个调用(第一个调用)仍然可以回溯——它会:

RESOLVENT: n(_)

结果是“是”。

这表明 Prolog 使用堆栈规则保存有关回溯的信息。您可能对用作 Prolog 实现模型的虚拟机感兴趣。它比您提到的执行模型要复杂得多,但是将 Prolog 翻译到 VM 将使您对 Prolog 的工作方式有更准确的理解。这是沃伦抽象机(WAM)。Hasan Aït-Kaci的教程是你能找到的最好的解释(它解释了剪辑,如果我没记错的话,原来的 WAM 描述中没有这个剪辑)。如果您不习惯抽象理论文本,您可以尝试先阅读 Peter van Roy 的文本:“ 1983-1993:顺序 Prolog 实现的奇迹年”。这篇文章很清楚,基本上是回顾了Prolog实现的历史,但是特别关注WAM。但是,它并没有展示cut是如何实现的。但是如果你仔细阅读它,你可能会挑选查看 Hasan 的教程并阅读他实现剪辑的部分。

于 2012-07-25T11:59:45.343 回答
1

您的测试目标中有一个额外的嵌套级别:

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.
于 2012-07-23T14:12:17.837 回答
1

虽然 mat 是正确的,因为您的程序不是纯序言(这与本章的标题是纯序言有关),不仅因为您使用了切割,而且因为您编写了处理其他谓词的谓词(纯序言是一阶逻辑的子集)这不是主要问题;你只是缺少回溯

虽然你确实有一个削减,但直到目标 n(f(X)) 成功才能达到这一点。但是,如您所知,这将失败,因此 prolog 将回溯并匹配第二个子句。

我看不出这与 6.1 中描述的模型有何矛盾(并且很难相信其他书籍会描述一个模型,在该模型中执行失败后将继续执行,从而允许削减以修剪其他解决方案)。无论如何,我发现得出“Prolog 实现不按照教科书中的执行模型行事”的结论与“编译器存在错误”非常相似,特别是因为“反例”的行为为它应该 (not(not(true)) 应该是真的)

于 2012-07-23T15:06:00.150 回答