How to avoid using assert and retractall in Prolog

2019-02-17 07:27发布

问题:

I often end up writing code in Prolog which involves some arithmetic calculation (or state information important throughout the program), by means of first obtaining the value stored in a predicate, then recalculating the value and finally storing the value using retractall and assert because in Prolog we cannot assign values to variable twice using is (thus making almost every variable that needs modification, global). I have come to know that this is not a good practice in Prolog. In this regard I would like to ask:

  1. Why is it a bad practice in Prolog (though i myself don't like to go through the above mentioned steps just to have have a kind of flexible (modifiable) variable)?

  2. What are some general ways to avoid this practice? Small examples will be greatly appreciated.

P.S. I just started learning Prolog. I do have programming experience in languages like C.

Edited for further clarification

A bad example (in win-prolog) of what I want to say is given below:

:- 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).

Then we can query like:

?- start.

Here, it is very trivial, but in real program and application, the above shown method of global variable becomes unavoidable. Sometimes the list given above like assert(value(0))... grows very long with many more assert predicates for defining more variables. This is done to make communication of the values between different functions possible and to store states of variables during the runtime of program.

Finally, I'd like to know one more thing: When does the practice mentioned above become unavoidable in spite of various solutions suggested by you to avoid it?

回答1:

The general way to avoid this is to think in terms of relations between states of your computations: You use one argument to hold the state that is relevant to your program before a calculation, and a second argument that describes the state after some calculation. For example, to describe a sequence of arithmetic operations on a value V0, you can use:

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

Notice how the state (in your case: the arithmetic value) is threaded through the predicates. The naming convention V0 -> V1 -> ... -> V scales easily to any number of operations and helps to keep in mind that V0 is the initial value, and V is the value after the various operations have been applied. Each predicate that needs to access or modify the state will have an argument that allows you to pass it the state.

A huge advantage of threading the state through like this is that you can easily reason about each operation in isolation: You can test it, debug it, analyze it with other tools etc., without having to set up any implicit global state. As another huge benefit, you can then use your programs in more directions provided you are using sufficiently general predicates. For example, you can ask: Which initial values lead to a given outcome?

?- state0_state(V0, given_outcome).

This is of course not readily possible when using the imperative style. You should therefore use constraints instead of is/2, because is/2 only works in one direction. Constraints are much easier to use and a more general modern alternative to low-level arithmetic.

The dynamic database is also slower than threading states through in variables, because it performs indexing etc. on each assertz/1.



回答2:

1 - it's bad practice because destroys the declarative model that (pure) Prolog programs exhibit.

Then the programmer must think in procedural terms, and the procedural model of Prolog is rather complicate and difficult to follow.

Specifically, we must be able to decide about the validity of asserted knowledge while the programs backtracks, i.e. follow alternative paths to those already tried, that (maybe) caused the assertions.

2 - We need additional variables to keep the state. A practical, maybe not very intuitive way, is using grammar rules (a DCG) instead of plain predicates. Grammar rules are translated adding two list arguments, normally hidden, and we can use those arguments to pass around the state implicitly, and reference/change it only where needed.

A really interesting introduction is here: DCGs in Prolog by Markus Triska. Look for Implicitly passing states around: you'll find this enlighting small example:

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

More generally, and for further practical examples, see Thinking in States, from the same author.

edit: generally, assert/retract are required only if you need to change the database, or keep track of computation result along backtracking. A simple example from my (very) old Prolog interpreter:

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 can be seen as the basic all solutions predicate. That code should be the very same from Clockins-Mellish textbook - Programming in Prolog. I used it while testing the 'real' findall/3 I implemented. You can see that it's not 'reentrant', because of the '$mark' aliased.