如何避免在Prolog中使用assert和retractall来实现全局(或状态)变量

如何避免在Prolog中使用assert和retractall来实现全局(或状态)变量,prolog,dcg,prolog-assert,implicit-state-passing,Prolog,Dcg,Prolog Assert,Implicit State Passing,我经常在Prolog中编写代码,其中包括一些算术计算(或整个程序中重要的状态信息),通过首先获取谓词中存储的值,然后重新计算值,最后使用retractall和assert存储值,因为在Prolog中,我们不能使用is将值分配给变量两次(因此几乎所有需要修改的变量都是全局的)。我已经知道,这在Prolog中不是一个好的实践。在这方面,我想问: 为什么在Prolog中这是一种不好的做法(尽管我自己不喜欢为了拥有一种灵活的(可修改的)变量而经历上述步骤) 避免这种做法的一般方法有哪些?小例子将不胜感激

我经常在Prolog中编写代码,其中包括一些算术计算(或整个程序中重要的状态信息),通过首先获取谓词中存储的值,然后重新计算值,最后使用
retractall
assert
存储值,因为在Prolog中,我们不能使用
is
将值分配给变量两次(因此几乎所有需要修改的变量都是全局的)。我已经知道,这在Prolog中不是一个好的实践。在这方面,我想问:

  • 为什么在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.
    
    在这里,它是非常平凡的,但在实际的程序和应用中,上面所示的全局变量方法变得不可避免。有时,上面给出的列表类似于断言(值(0))。。。随着更多用于定义更多变量的断言谓词的增加而增长。这样做是为了使不同函数之间的值通信成为可能,并在程序运行期间存储变量的状态

    最后,我还想知道一件事:
    尽管您提出了各种解决方案,但上述实践何时不可避免?

    避免这种情况的一般方法是考虑计算状态之间的关系:在计算之前,您使用一个参数来保持与程序相关的状态,第二个参数描述了经过计算后的状态。例如,要描述对值
    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

    1上执行索引等操作-这是一种不好的做法,因为它破坏了(纯)Prolog程序所展示的声明性模型

    然后程序员必须从过程的角度来思考,Prolog的过程模型相当复杂,很难遵循

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

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

    这里有一个非常有趣的介绍:Markus Triska。查找隐式传递状态的
    :您会发现这个启发性的小示例:

    num_leaves(nil), [N1] --> [N0], { N1 is N0 + 1 }.
    num_leaves(node(_,Left,Right)) -->
              num_leaves(Left),
              num_leaves(Right).
    
    更一般地说,有关更多的实际示例,请参见,来自同一作者

    编辑:通常,仅当您需要更改数据库或沿回溯跟踪计算结果时,才需要断言/收回。我(非常)老朋友的一个简单例子:

    findall/3可以看作是基本谓词。这些代码应该与Clockins Mellish教科书中的相同——用Prolog编程。我在测试“真实”findall/3i时使用了它。您可以看到它不是“可重入的”,因为有“$mark”别名

    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'.