Reasonable way to combine a retractall and assert operation?

I’m using: SWI-Prolog version 8.0.2.

I find myself writing the pattern below a lot, when I want to replace a singleton item in the Prolog database:

retractall(some_term_with_three_args(_,_,_)),
assert(some_term_with_three_args(arg_1, arg_2, arg_3)).

So I created my own replace predicate that combines both:

replace_singleton_term(Term) :-
    functor(Term, PredName, Arity),
    functor(RetractTemplate, PredName, Arity),
    retractall(RetractTemplate),
    assert(Term), !.

My concern is that there might be some unforeseen consequence in using functor() to create the template for removing all occurrences of any existing database element with the same predicate name as that in Term and the same arity. Is there anything about what I’m doing here that could cause some unforeseen problem I might not expect to happen? Any other comments?

I find it usually more useful to write predicates like this:

   replace_term_with_three_args(A1,A2,A3) :-
       retractall(some_term_with_three_args(_,_,_)),
       assert(some_term_with_three_args(A1, A2, A3)).

As the project moves on, I usually need to do something special when the term is replaced (e.g. collect a count or handle some especial exceptional condition). With individual replace predicates like above it is easy to add the special condition to the replace_term_with_three_args/3 predicate, something that you would find hard to do with the generic replace.

I think your first solution is pretty much ok. I’ve been thinking about this problem before. We want something that does the above in an atomic way. I’m still doubting how though. It is fairly trivial to solve this one clause predicate case, but possible we should seek a more general solution such as proper transaction support.

Note that this type of programming for dealing with state can be really slow. Consider:

l(N) :-
    assert(n(N)),
    count_down.

count_down :-
    retract(n(N0)), 
    N1 is N0-1,
    asserta(n(N1)),
    (   N1 == 0
    ->  true
    ;   count_down
    ).

vs.

count_down(N) :-
    N > 0,
    !,
    N2 is N - 1,
    count_down(N2).
count_down(0).

This gives me the results below. All except the SWISH are run on the same hardware (Intel i7-3770). SWISH runs on slower hardware while the large number of threads there cause clause garbage collection to happen much less frequently and be more expensive.

test SWI SWISH XSB YAP
l(1 000 000) 0.961 81.211 5.528 crash
count_down(1 000 000) 0.059 0.123 0.05 0.059
2 Likes

Is the primary motivation for creating an atomic transaction to make it thread-safe, so that the retractall + assert pair doesn’t get split by a thread switch?

1 Like

More that another thread that wants to have the current value, but gets a failure. It is hard to work around. Using a mutex for both reading and writing works, but scales poorly. If a double answer is not a problem you can also do

    clause(n(N0), true, Ref),
    N1 is N0-1,
    asserta(n(N1)),
    erase(Ref).

Now if the reader uses once(n(N)) you get consistent results.

1 Like

If the operation (retractall + assert) takes place in code in a PEngines instance that was loaded via the src_text parameter, I assume that the multi-thread issue is not a problem, since the code would be local to only one particular PEngines instance, correct?

Yes. The slowdown still happens as Prolog doesn’t know this code can only be accessed by a single thread. Clause GC makes a sweep through the environment stacks of all threads to figure out which predicates are active under which generation.

2 Likes