Isolating Dynamic Predicates

Hello,

In the following code, I was expecting the second test to fail because no facts of the dynamic ev/2 predicate were defined in t2, but the ev/2 facts are being shared by t1 and t2:

:- begin_tests(probability).
:- use_module(probability).

test(frequency) :-
  t1:import(probability:ev/2),
  t1:import(probability:f/3),
  assert(t1:ev(coin_toss, heads)),
  assert(t1:ev(coin_toss, tails)),
  assertion(t1:f(heads, [], 0)),
  assertion(t1:f(heads, [tails, tails], 0)),
  assertion(t1:f(heads, [heads, tails], 1)),
  assertion(t1:f(tails, [heads, tails], 1)),
  assertion(t1:f(tails, [tails, heads, tails], 2)),
  assertion(t1:f(tails, [tails, tails, tails], 3)).

test(probability) :-
  t2:import(probability:ev/2),
  t2:import(probability:p/3),
  assertion(t2:p(heads, [], 0.0)),
  assertion(t2:p(heads, [tails, tails], 0.0)),
  assertion(t2:p(heads, [heads, tails], 0.5)),
  assertion(t2:p(tails, [heads, tails], 0.5)),
  assertion(t2:p(tails, [tails, heads, heads, heads], 0.25)),
  assertion(t2:p(tails, [tails, tails, tails], 1.0)).

:- end_tests(probability).

Is there a way to isolate the facts of dynamic predicates in t1 and t2?

Thanks,
Quenio

t1 and t2 both import from probability and assert/retract to the imported predicates asserts/retract to/from the module from which the predicate is imported. So they indeed share. There is no benefit in creating t1 and t2. The normal way would be to use a setup and cleanup option. You could define a setup helper that gets as argument a number of things assert and that instantiates a list of clause references and then a cleanup that calls erase/1 to get rid of these specific clauses.

The alternative is to wrap the test in snapshot/1. This makes me think it might be nice to add an option shapshot(true) to tests that will do the wrapping for you, so you can write

test(..., [snapshot(true)]) :-
    ...
2 Likes

FYI for others who may be lost on that statement.

Here is real world code

I know how to isolate tests with the setup-cleanup pattern. This test is just an excuse to check if world isolation is possible. What I really want to do is to isolate worlds, just like it is possible with LogTalk objects. I want to be able to do it with modules without having to adopt LogTalk.

This is what worked for me:

:- begin_tests(probability).
:- use_module(probability).

test_isolated(Goal) :-
  engine_create(_, Goal, E),
  engine_next(E, _),
  engine_destroy(E).

test(frequency) :-
  test_isolated(test_frequency).

test_frequency :-
  assert(ev(coin_toss, heads)),
  assert(ev(coin_toss, tails)),
  assertion(f(heads, [], 0)),
  assertion(f(heads, [tails, tails], 0)),
  assertion(f(heads, [heads, tails], 1)),
  assertion(f(tails, [heads, tails], 1)),
  assertion(f(tails, [tails, heads, tails], 2)),
  assertion(f(tails, [tails, tails, tails], 3)).

test(probability) :-
  test_isolated(test_probability).

test_probability :-
  assert(ev(coin_toss, heads)),
  assert(ev(coin_toss, tails)),
  assertion(p(heads, [], 0.0)),
  assertion(p(heads, [tails, tails], 0.0)),
  assertion(p(heads, [heads, tails], 0.5)),
  assertion(p(tails, [heads, tails], 0.5)),
  assertion(p(tails, [tails, heads, heads, heads], 0.25)),
  assertion(p(tails, [tails, tails, tails], 1.0)).

:- end_tests(probability).

As long as:

:- thread_local ev/2

But this solution does not involve modules; requires engines.

Logtalk compiles to ISO Prolog, so anything Logtalk can do can also be done without. Of course, it offers a lot of stuff that nicely fits together and thus if you need enough of that it is worth looking at.

For isolation, SWI-Prolog offers modules, threads (engines) and transaction/snapshots. Which one to use depends on what exactly you want. Modules can create a “world”, but you need a way to make the code access a particular module/world. There are two ways for that: pass the module (world) as an argument and use Module:Goal qualified goals or use meta predicates. You have seen thread_local/1 predicates. snapshot/1 allows a goal to use the dynamic DB such that the changes are destroyed at completion and if you use multiple threads, the threads have no access to each others modifications (they do have access to the state before the snapshot was created). As all clauses involved in all threads in a snapshot are in one physical predicate, you will get serious overhead with many threads making large modifications inside the snapshot. That is not a problem for thread_local/1 predicates. There, each thread has its own physical predicate.

The good old traditional way is to simply add an argument to the dynamic predicate to reflect the world. This can also be attractive. SWI-Prolog’s JIT multi-argument indexing can make it fairly efficient, depending on the arguments and call patterns.

2 Likes

I am learning towards engines, because I want to implement a system of cooperating agents, where each agent has its own world view. They will likely share and perform similar roles, which can be encapsulated as modules, but each agent should be able to run independently and have its own world view, so that they can evolve independently. They also need to be able to communicate, which could be done via engine_post and engine_fetch. And some agents may have the supervisor role, which could be implemented via engine_next and engine_yield.

1 Like

Wow Quenio. That sounds extremely abstract. I don’t really get what you’re aiming at (but with me that’s the rule rather than the exception). More expert users will probably know how to help. Cheers