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