Abusing "dynamic" predicates

Is this code abusing dynamic predicates?

:- dynamic(init_meters/0).
init_meters() :-
    asserta(meter(health, 5)),
    asserta(meter(spirit, 5)),
    asserta(meter(supply, 5)).

:- dynamic(update_meter/2).
update_meter(Meter, Value) :-
    meter(Meter, Prev),
    retract(meter(Meter, Prev)),
    asserta(meter(Meter, Value)),
    meter(Meter, Value).

:- dynamic(read_meter/2).
read_meter(Meter, Value) :-
    meter(Meter, Value).

Considering:

  • It’s for maintaining a Tabletop RPG game, not a Healthcare system - so, only fictional people will die.
  • I understand this code is mimicking state. Googling for state management in Prolog resulted in different solutions. But for this task, the code seems pretty convenient (since there are only limited sets of values - low cardinality). Just checking to make sure it’s not blasphemy! But I like to learn different approaches for state management in different contexts too.

Thank you!

Pure prolog would not use assert or retract, but that’s okay if you’re choosing not to be purist.

You are misusing :- dynamic. You should have only one dynamic declaration:

:- dynamic(meter/2).

Because that’s the one you are asserting and retracting.

read_meter, there’s no reason for it to exist. Just use meter(Meter, Value) directly.

update_meter, the last line of the predicate is redundant. You already know meter exists with that, so no reason to call it.

3 Likes

You are not abusing dynamic predicates, you are just using them.

Some more nitpicks, your definition of init_meters/0 has a latent bug. If you call it more than once within a single “session” you will end up with duplicate entries. I am using your code example exactly as is:

?- init_meters.
true.

?- meter(health, H).
H = 5.

?- init_meters.
true.

?- meter(health, H).
H = 5 ;
H = 5.

As a rule of thumb, if you do any initialization, start with a retractall/1:

init_meters :-
    retractall(meter(_, _)),
    /* assert rows */

There are other potential issues with code like this. Remember that retract/1 is nondet which means that it will leave a choice point if multiple rows match. This might or might not cause a bug later on.

3 Likes

Using dynamic predicates this way is the classical and portable Prolog solution. Many Prolog systems have alternatives though. Dynamic predicates are rather heavy weight, especially in SWI-Prolog where they are thread-safe and lock-free. That is great for executing dynamic code, but high frequency assert/retract tends to pile up dead clauses (because we are not sure they are not in use anywhere) that causes slowdown. Eventually they are reclaimed.

Global variables are a sensible alternative. See nb_setval/2 and friends. Another alternative is to use a dict that you pass around and nb_set_dict/3. That is more or less the same as passing a struct/record/object/… in conventional languages. You can combine the two and put the dict in a global variable and then manipulating it.

That is non backtrackable state. You may also want backtrackable state, which is another story with quite a few alternatives :slight_smile:

3 Likes