Yeah – side effects are PITA. Especially for unit testing.
What worked for me:
- add new rules and facts and to some module, m. Something like:
m:assert( .... )
- retract the entirety of the module in a Python context manager’s exit. Here’s what I have for pyswip:
@staticmethod
@contextmanager
def module_context(module_name=None, prolog=None,
finalizer=abolish_module_contents,
**kw,
) -> Iterator[Prolog]:
p = ModuleContext(module_name=module_name,
prolog=prolog,
**kw)
try:
yield p
finally:
if finalizer is not None:
if callable(finalizer):
finalizer(module_name=p.module_name, prolog=p)
else:
p.run(finalizer)
The yielded object is a Prolog object monkey-patched to wrap queries and asserts as: “{modulename}:({original_query})”.
And where finalizer does:
prolog.query_all(f"forall(current_predicate({module_name}:P), "
f"(P\\={module_name}:pyrun/2, abolish({module_name}:P)))")
You’d use it:
with module_context('m') as prolog:
prolog.query(s) # Does m:s
Seemed to work well except that many predicates need to be made aware of module prefixes in their variables. For example, I had to modify a flatmap utility predicate
:- meta_predicate
flatmap(:,*,*).
flatmap(_, [], []).
% flatmap(P, [[X0, X1, ...]], [Y0, Y1, ...]) where call(P, Xi, Yi)
flatmap(M:P, [H|T], Ls_):-
call(M:P, H, Hs),
append(Hs, Ls__, Ls_),
flatmap(M:P, T, Ls__).
Is worth it to maintain idempotence.
I also used the same pattern to enable multi-threaded queries from Python by using one module name per thread.