6

我经常在 Prolog 中编写代码,其中涉及一些算术计算(或在整个程序中很重要的状态信息),首先获取存储在谓词中的值,然后重新计算值,最后使用存储值retractallassert因为在 Prolog 中,我们不能使用两次为变量赋值is(因此几乎每个需要修改的变量都是全局的)。我开始知道这在 Prolog 中不是一个好习惯。对此我想问:

  1. 为什么在 Prolog 中这是一种不好的做法(尽管我自己不喜欢通过上述步骤只是为了拥有一种灵活的(可修改的)变量)?

  2. 有哪些一般方法可以避免这种做法?小例子将不胜感激。

PS我刚开始学习Prolog。我确实有 C 等语言的编程经验。

为进一步澄清而编辑

下面给出了我想说的一个不好的例子(在 win-prolog 中):

:- dynamic(value/1).
:- assert(value(0)).

adds :- 
   value(X),
   NewX is X + 4,
   retractall(value(_)),
   assert(value(NewX)).

mults :-
   value(Y),
   NewY is Y * 2,
   retractall(value(_)),
   assert(value(NewY)).

start :-
   retractall(value(_)),
   assert(value(3)),
   adds,
   mults,
   value(Q),
   write(Q).

然后我们可以像这样查询:

?- start.

在这里,这是非常琐碎的,但在实际程序和应用中,上述全局变量的方法变得不可避免。有时,上面给出的列表如assert(value(0))... 会变得很长,其中包含更多用于定义更多变量的断言谓词。这样做是为了使不同函数之间的值通信成为可能,并在程序运行期间存储变量的状态。

最后,我还想知道一件事:尽管您提出了各种避免方法,但上述做法何时变得不可避免?

4

2 回答 2

6

避免这种情况的一般方法是考虑计算状态之间的关系:在计算之前使用一个参数来保存与程序相关的状态,使用第二个参数来描述一些计算之后的状态。例如,要描述对 value 的一系列算术运算V0,您可以使用:

state0_state(V0, V) :-
    operation1_result(V0, V1),
    operation2_result(V1, V2),
    operation3_result(V2, V).

请注意状态(在您的情况下:算术值)如何通过谓词线程化。命名约定V0-> V1-> ... ->V可以轻松扩展到任意数量的操作,并有助于记住这V0是初始值,并且V是应用各种操作后的值。每个需要访问或修改状态的谓词都有一个参数,允许您将状态传递给它。

像这样通过线程处理状态的一个巨大优势是,您可以轻松地单独推理每个操作:您可以对其进行测试、调试、使用其他工具进行分析等,无需设置任何隐式全局状态。另一个巨大的好处是,只要您使用足够通用的谓词,您就可以在更多方向上使用您的程序。例如,您可以问:哪些初始值会导致给定结果?

?- state0_state(V0, given_outcome).

这在使用命令式风格时当然不容易。因此,您应该使用约束而不是is/2,因为is/2仅在一个方向上起作用。约束更容易使用,并且是低级算术的更通用的现代替代方案。

动态数据库也比通过变量线程化状态慢,因为它对每个assertz/1.

于 2013-09-25T13:27:08.793 回答
4

1 - 这是不好的做法,因为破坏了(纯)Prolog 程序展示的声明性模型。

那么程序员必须从程序的角度来思考,而Prolog的程序模型相当复杂且难以遵循。

具体来说,我们必须能够在程序回溯时确定断言知识的有效性,即遵循已经尝试过的替代路径,这(可能)导致断言。

2 - 我们需要额外的变量来保持状态。一种实用的,也许不是很直观的方法是使用语法规则(DCG)而不是简单的谓词。语法规则被翻译为添加两个列表参数,通常是隐藏的,我们可以使用这些参数隐式传递状态,并仅在需要时引用/更改它。

这里有一个非常有趣的介绍:Markus Triska的 Prolog 中的 DCG。寻找Implicitly passing states around:你会发现这个启发性的小例子:

num_leaves(nil), [N1] --> [N0], { N1 is N0 + 1 }.
num_leaves(node(_,Left,Right)) -->
          num_leaves(Left),
          num_leaves(Right).

更一般地,有关更多实际示例,请参阅同一作者的Thinking in States

编辑:通常,仅当您需要更改数据库或沿回溯跟踪计算结果时才需要断言/撤回。我的(非常)旧的Prolog 解释器的一个简单示例:

findall_p(X,G,_):-
    asserta(found('$mark')),
    call(G),
    asserta(found(X)),
    fail.
findall_p(_,_,N) :-
    collect_found([],N),
    !.
collect_found(S,L) :-
    getnext(X),
    !,
    collect_found([X|S],L).
collect_found(L,L).
getnext(X) :-
    retract(found(X)),
    !,
    X \= '$mark'.

findall/3 可以看作是基本的所有解决方案谓词。该代码应该与 Clockins-Melish 教科书 - Prolog 编程中的代码完全相同。我在测试我实现的“真正的” findall/3 时使用了它。您可以看到它不是“可重入的”,因为“$mark”有别名。

于 2013-09-25T13:35:53.707 回答