How to call predicate in test that is called from outside of test?

How can a predicate parent/2 be called if it is created in a test, but the predicate that calls parent/2, (ancestor/2) is outside of the test and the call to ancestor/2 is initiated by a test?

Test module  Main module    Test module
test         -> ancestor/2  -> parent/2

Here is a complete minimal example.

The main module shows a set of predicates (contains/3 and ancestor/3) that work if I pass the module as an argument.

The main module also shows a set of predicates that I would like to use (contains/2 and ancestor/2) which don’t pass the module in an argument.

There are also two separate tests modules that tests these predicates.

assembly_a test the predicates in main that pass the module as an argument and succeed.

assembly_b test the predicates in main that do not pass the module as an argument and these fail when I would like them to succeed.

Click triangle to see minimal example
:- module(main, []).

contains(Module,Assembly,Part_type) :-
    ancestor(Module,Assembly,Part_type), !.

ancestor(Module,A,B) :-
    Module:parent(A,B).
ancestor(Module,A,C) :-
    Module:parent(A,B),
    ancestor(Module,B,C).

contains(Assembly,Part_type) :-
    ancestor(Assembly,Part_type), !.

ancestor(A,B) :-
    parent(A,B).
ancestor(A,C) :-
    parent(A,B),
    ancestor(B,C).

% ---------------------------------------------------------------------------------------------------------------------

:- begin_tests(assembly_a).

parent(bike,frame).
parent(bike,drivechain).
parent(bike,wheel).
parent(wheel,spokes).
parent(wheel,rim).
parent(wheel,hub).
parent(drivechain,crank).
parent(drivechain,pedal).
parent(drivechain,chain).

% Test: parent/2 - module verification
test(01) :-
    predicate_property(parent(_,_), implementation_module(Module)),
    assertion( Module == plunit_assembly_a ).

% Test: contains/3 - no error - 1 level
test(02) :-
    contains(plunit_assembly_a,bike,frame).

% Test: contains/3 - no error - 2 levels
test(03) :-
    contains(plunit_assembly_a,bike,crank).

% Test: contains/3 - fail
test(04,[fail]) :-
    contains(plunit_assembly_a,bike,a).

:- end_tests(assembly_a).

% -----------------------------------------------------------------------------

:- begin_tests(assembly_b).

:- meta_predicate parent(2,?).

parent(bike,frame).
parent(bike,drivechain).
parent(bike,wheel).
parent(wheel,spokes).
parent(wheel,rim).
parent(wheel,hub).
parent(drivechain,crank).
parent(drivechain,pedal).
parent(drivechain,chain).

% Test: parent/2 - module verification
test(01) :-
    predicate_property(parent(_,_), implementation_module(Module)),
    assertion( Module == plunit_assembly_b ).

% Test: contains/3 - no error - 1 level
test(02) :-
    contains(bike,frame).

% Test: contains/3 - no error - 2 levels
test(03) :-
    contains(bike,crank).

% Test: contains/3 - fail
test(04,[fail]) :-
    contains(bike,a).

:- end_tests(assembly_b).

I am thinking meta_predicate/1 should work as given in assembly_b,
:- meta_predicate parent(2,?). but it did not.

Previous related post: How can the module for a term be identified?

1 Like

Related: https://stackoverflow.com/questions/57341339/structuring-swi-prolog-code-into-modules-for-unit-testing-for-varying-data-sets/57347452#57347452

1 Like

Thanks to Paulo his response put me on a path that lead to an acceptable solution.

While I did not use any of the answers noted in the referenced StackOverflow question, the answer by @PaulBrownMagic was most helpful in this case.

The only way I found to use ancestor/2 without adding a module argument to the predicate was to put the parent/2 predicates in to the same module as ancestor/2.

Since hard coding them into the same module as ancestor/2 would cause a possible conflict of so called different worlds, the idea to use the test setup and cleanup to assert them into the module was used.

To allow the code to dynamically find the name of module for ancestor/2, predicate_property/2 was used.

To suppress the warnings about parent/2 not being defined dynamic/1 was added.

Click triangle to see working code
:- module(main, [
            contains/2,
            ancestor/2
    ]).

:- dynamic parent/2.

contains(Assembly,Part_type) :-
    ancestor(Assembly,Part_type), !.

ancestor(A,B) :-
    parent(A,B).
ancestor(A,C) :-
    parent(A,B),
    ancestor(B,C).

:- begin_tests(variation_d).

path_setup :-
    predicate_property(ancestor(_,_), implementation_module(Module)),
    assert( Module:parent(bike,frame)       ),
    assert( Module:parent(bike,drivechain)  ),
    assert( Module:parent(bike,wheel)       ),
    assert( Module:parent(wheel,spokes)     ),
    assert( Module:parent(wheel,rim)        ),
    assert( Module:parent(wheel,hub)        ),
    assert( Module:parent(drivechain,crank) ),
    assert( Module:parent(drivechain,pedal) ),
    assert( Module:parent(drivechain,chain) ).

path_cleanup :-
    predicate_property(ancestor(_,_), implementation_module(Module)),
    retractall( Module:parent(_,_) ).

% Test: ancestor/2 - module verification
test(01) :-
    predicate_property(ancestor(_,_), implementation_module(Module)),
    assertion( Module == main ).

% Test: contains/3 - no error - 1 level
test(02,[setup(path_setup),cleanup(path_cleanup)]) :-
    contains(bike,frame).

% Test: contains/3 - no error - 2 levels
test(03,[setup(path_setup),cleanup(path_cleanup)]) :-
    contains(bike,crank).

% Test: contains/3 - fail
test(04,[fail,setup(path_setup),cleanup(path_cleanup)]) :-
    contains(bike,a).

:- end_tests(variation_d).

Example run:

?- run_tests.
% PL-Unit: variation_d .... done
% All 4 tests passed
true.

EDIT

Of interest:
library(persistency): Provide persistent dynamic predicates
A Parameterised Module System for Constructing Typed Logic Programs
More on Transitive Closure

1 Like

What you have is an example of the “many worlds” design pattern:

https://logtalk.org/2019/11/13/many-worlds-design-pattern.html

2 Likes

So long as you read the discussion too: my answer there is a dirty hack for when you can’t implement the clean solution with Logtalk. When you have many-worlds, Prolog’s modules start to struggle.

2 Likes

As is, this has no clean solution in the plain SWI-Prolog module system. One possible option is to define ancestor/2 as a meta predicate and import it into the module that provides the parent/2 relation. That would look like

:- meta_predicate ancestor(:,?).

ancestor(M:A, B) :-
    ancestor(A,B,M).

ancestor(A,B,M) :-
    M:parent(A,B).
ancestor(A,C,M) :-
    M:parent(A,B),
    ancestor(B, C, M).

This is rather ugly. I’d probably go for

:- meta_predicate ancestor(2, ?, ?).

ancestor(Rel, A, B) :-
    call(Rel, A, B).
ancestor(Rel, A, C) :-
    call(Rel, A, B),
    ancestor(Rel, B, C).

After which you can rename ancestor to something like transitive_closure.

E.g., XSB has parameterized modules. Possibly SWI-Prolog should get these too at some time …

1 Like

After finding my answer I went back into the SWI-Prolog source on GitHub looking to see if this was a pattern you used and ran in the XSB test suite

Now knowing about parameterized modules in there will take a much closer look. Thanks. :smiley:

XSB parameterized modules are a limited implementation of +20 years old parametric entity ideas that are found in several systems, including Logtalk. An overview of those ideas and systems can be found in:

Programming Patterns for Logtalk Parametric Objects. Paulo Moura. Applications of Declarative Programming and Knowledge Management, Lecture Notes in Artificial Intelligence Vol. 6547, April 2011

2 Likes