让我们首先习惯于声明式阅读逻辑程序。
Prolog 程序以声明方式声明什么是真的。
例如
natural_number(0).
natural_number(s(X)) :-
natural_number(X).
第一个子句声明:0 是一个自然数。
第二个子句指出:如果 X 是自然数,那么 s(X) 是自然数。
现在让我们考虑更改此程序的效果。例如,当我们改变这两个子句的顺序时,会发生什么变化?
natural_number(s(X)) :-
natural_number(X).
natural_number(0).
声明式地,交换子句的顺序不会以任何方式改变程序的预期含义(析取是可交换的)。
操作上,即考虑到Prolog的实际执行策略,显然不同的子句顺序往往会产生显着的差异。
然而,无论选择的子句顺序如何,都保留了纯 Prolog 代码的一个非常好的特性:
如果查询Q成功关于排序O1的子句,那么
Q 不会失败,但顺序不同 O2。
请注意,我不是说Q总是也成功以不同的顺序:这是因为查询也可能循环或产生不同排序的错误。
对于两个查询Q1 和Q2,我们说G1更一般如果它包含G2 就句法统一而言。例如,查询?- parent_child(P, C). 比查询?- parent_child(0, s(0)).更通用。
现在,对于纯 Prolog 程序,另一个非常好的属性成立:
如果查询Q1 成功,那么每个更一般的查询Q2 都不会
失败。
再次注意,Q2 可能会循环而不是成功。
现在考虑您提到的var/1 的情况,并考虑相关的谓词nonvar/1。假设我们有:
my_pred(V) :-
nonvar(V).
什么时候成立?显然,如果参数不是变量,则它成立。
正如所料,我们得到:
?- my_pred(a).
true.
但是,对于更一般的查询?- my_pred(X).,我们得到:
?- my_pred(X).
false.
这样的谓词被称为非单调,由于这个属性,你不能把它当作一个真正的关系:这是因为上面的答案false在逻辑上意味着有没有任何解决方案,但在前面的示例中,我们看到有解决方案。因此,不合逻辑地,通过添加约束构建的更具体查询会使查询成功:
?- X = a, my_pred(X).
true.
因此,对此类谓词进行推理非常复杂,以至于用它们进行编程一点也不好玩。它使声明式调试变得不可能,并且很难声明任何保留的属性。例如,仅在上述连接查询中交换子目标的顺序就会使其失败:
?- my_pred(X), X = a.
false.
因此,我强烈建议留在 Prolog 的纯单调子集内,这允许按照上面概述的路线进行声明性推理。
CLP(FD) 约束、dif/2 等在这个意义上都是纯:你不能欺骗这些谓词给出逻辑上无效的答案,无论你使用何种模式、顺序等使用它们。 if_/3 也满足这个属性。另一方面,var/1、nonvar/1、integer/1、!/0、带有副作用的谓词等都是在逻辑上引用了正在描述的声明性世界之外的东西,因此不能被考虑纯的。
编辑:澄清一下:我在这里提到的好属性绝不是详尽无遗的。纯 Prolog 代码展示了许多其他非常有价值的属性,通过这些属性您可以感知逻辑编程的荣耀。例如,在纯 Prolog 代码中,添加一个子句最多可以扩展,而不是缩小解决方案的集合;添加一个目标最多可以缩小,从不扩展,等等。
使用一个额外的逻辑原语可能并且通常会破坏许多这些属性。因此,例如,每次使用 !/0 时,都将其视为切纯洁之心,并为伤害这些属性而感到遗憾和羞耻。
一本好的 Prolog 书至少会开始引入或包含许多提示,以鼓励这种声明式观点,指导您考虑更一般的查询、保留的属性等。糟糕的 Prolog 书不会对此多说,而且通常最终会使用那些破坏语言最有价值和最美丽属性的不纯语言元素。
一个很棒的 Prolog 教学环境,它广泛使用这些属性来实现声明式调试,称为GUPU,我强烈建议查看这些想法。 Ulrich Neumerkel 慷慨地提出了一个在他的环境中使用的核心思想,部分可用为library(diadem)。有关如何以声明方式调试意外失败的目标的良好示例,请参阅源文件:该库系统地构建仍然失败的查询的概括。当然,这种推理完美地使用纯代码。