2
votes

This may be specific to the SWI Prolog module system.

Suppose we have three Prolog modules (in the SWI-Prolog module system):

  • robin (in file robin.pl)
  • arthur (in file arthur.pl)
  • helper (in file helper.pl).

Predicates robin:robin/0 (i.e. predicate robin_hood/0 in module robin) and predicate arthur:arthur/0 call predicate helper:helper/2 (which is exported by module helper).

Predicate helper:helper/2 then should call a predicate toolshed/1, which is different depending on the caller modules. I would like helper/2 to call the toolshed/1 predicate associated with the predicate that calls helper/2.

In Java, one would pass an interface with a toolshed() method to helper(), so that helper() can call the interface and end up at the correct implementation.

How do I do it in Prolog?

calls

Example code:

robin.pl

:- module(robin,
          [
           robin/0
          ,toolshed/1
          ]).

:- use_module(library('helper.pl')).

robin :- helper(friar_tuck,help).

toolshed('a katana made by mastersmith Masamune').
toolshed('an ancient shovel with a sharpened blade').
toolshed('an Uzi 9mm with Norinco markings').

arthur.pl

:- module(arthur,
          [
           arthur/0
          ,toolshed/1
          ]).

:- use_module(library('helper.pl')).

arthur :- helper(merlin,help).

toolshed('a slightly musty grimoire').
toolshed('a jar of mandragore').
toolshed('a fresh toadstool').

helper.pl

:- module(helper,
          [
          helper/2
          ]).

helper(friar_tuck,help) :-
   format("I will help you rout the Sheriff of Nottingham's men!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

helper(merlin,help) :-
   format("I will help you rout Mordred's army!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

Put them into a director testing:

testing
├── arthur.pl
├── helper.pl
└── robin.pl

Launch swipl, set the library search path and load arthur.pl:

?- assertz(file_search_path(library,'/home/me/testing')).
true.

?- use_module(library('arthur.pl')).
true.

?- arthur.
I will help you rout Mordred's army!
I found these tools: ['a fresh toadstool','a jar of mandragore','a slightly musty grimoire']
Have at them!
true.

So this works. toolshed/1 is exported by module arthur and visible (and callable unqalified) by module helper even though helper does not import arthur.pl (not quite sure how that works, mabye the exported predicates of all modules belonging to predicates currently on the stack are visible and accessible unqalified?).

But I can't load robin.pl too:

?- use_module(library('robin.pl')).
ERROR: import/1: No permission to import robin:toolshed/1 into user (already imported from arthur)
true.

Ok, this is not surprising. But how do I get the result I want? I want to see this:

?- use_module(library('robin.pl')).
true.

?- robin.
I will help you rout the Sheriff of Nottingham's men!
I found these tools: ['a katana made by mastersmith Masamune','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!
true.
4
I tried to put multifile/1 directives for toolshed/1 in both arthur.pl and robin.pl but that yields the same problem.David Tonhofer
You can make it work with multifile predicates. See my alternative answer. Understanding the pros and cons of all these alternatives is a good learning exercise.Paulo Moura
Can't you make helper/2 into helper/3 and pass it the list of tools found in the local toolshed/1?Paul Brown
@PaulBrown Of course you can "serialize out" the results in this case (in particular because the list is not too long), but in general scenarios that would not be the case. In an Inversion-of-Control approach, the "toolshed" is some called-back predicate for which it only makes sense to call it in the future. Imaginge "helper" as an event handler, "toolshed" as the event handling code, and the modules as pluggable components.David Tonhofer
Yes, I wondered if this was to be representative of that more general case or was the actual issue to be resolved.Paul Brown

4 Answers

3
votes

Using the SWI-Prolog proprietary module-transparent mechanism do provide a working alternative. But be aware that this mechanism is not only marked as "Direct usage by programmers is deprecated." but also have other problems not mentioned in that documentation.

In this solution, we make the helper/2 predicate module transparent:

:- module(helper, [helper/2]).

:- module_transparent(helper/2).

helper(friar_tuck,help) :-
   format("I will help you rout the Sheriff of Nottingham's men!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

helper(merlin,help) :-
   format("I will help you rout Mordred's army!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

The other modules are then simplified to:

:- module(arthur, [arthur/0]).

:- use_module('helper.pl').

arthur :- helper(merlin,help).

toolshed('a slightly musty grimoire').
toolshed('a jar of mandragore').
toolshed('a fresh toadstool').

and:

:- module(robin, [robin/0]).

:- use_module('helper.pl').

robin :- helper(friar_tuck,help).

toolshed('a katana made by mastersmith Masamune').
toolshed('an ancient shovel with a sharpened blade').
toolshed('an Uzi 9mm with Norinco markings').

We then get:

?- [helper, arthur, robin].
true.

?- arthur.
I will help you rout Mordred's army!
I found these tools: ['a fresh toadstool','a jar of mandragore','a slightly musty grimoire']
Have at them!
true.

?- robin.
I will help you rout the Sheriff of Nottingham's men!
I found these tools: ['a katana made by mastersmith Masamune','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!
true.

That said, this and the other modules solutions have several issues and don't scale well to more complex cases as the features that you're looking for (notably, interfaces/protocols as a first class construct) don't exist in Prolog modules systems and hacks fall short (see e.g. https://logtalk.org/blog.html?tag=half+broken+hacks).

2
votes

Looks like I stumbled upon a solution:

The new helper.pl

It has a modified helper/2: now helper/3 which accepts the module name of the caller as third argument and uses it to qualify the call to toolshed/1:

(is it possible to find out by which module one is currently being called? without creating a stack trace?)

:- module(helper,
          [
          helper/3
          ]).

helper(friar_tuck,help,Module) :-
   format("I will help you rout the Sheriff of Nottingham's men!~n"),
   setof(X,Module:toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

helper(merlin,help,Module) :-
   format("I will help you rout Mordred's army!~n"),
   setof(X,Module:toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

The new arthur.pl

Does not export toolshed/1 and passes the name of the arthurmodule in the call tohelper:helper/3`. (Is it possible to find the name of the current module? This hardcoding is an accident waiting to happen).

:- module(arthur,
          [
           arthur/0
          ]).

:- use_module(library('helper.pl')).

arthur :- helper(merlin,help,arthur).  % Give module name as third argument

toolshed('a slightly musty grimoire').
toolshed('a jar of mandragore').
toolshed('a fresh toadstool').

The new robin.pl

:- module(robin,
          [
           robin/0
          ]).

:- use_module(library('helper.pl')).

robin :- helper(friar_tuck,help,robin). % Give module name as third argument

toolshed('a katana made by mastersmith Masamune').
toolshed('an ancient shovel with a sharpened blade').
toolshed('an Uzi 9mm with Norinco markings').

And it indeed works. Both modules arthur and robin can be loaded at the same time because toolshed/1 is no longer exported, but can still be used from helper:helper/3.

Start swipl, then:

?- assertz(file_search_path(library,'/home/me/testing')).
true.

Now I can load both modules, as there is no clash on the unexported toolshed/1:

?- use_module(library('robin.pl')).
true.

?- use_module(library('arthur.pl')).
true.

And calling toolshed/1 in qualified fashion works well:

?- robin.
I will help you rout the Sheriff of Nottingham's men!
I found these tools: ['a katana made by mastersmith Masamune','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!
true.

?- arthur.
I will help you rout Mordred's army!
I found these tools: ['a fresh toadstool','a jar of mandragore','a slightly musty grimoire']
Have at them!
true.

This seems right enough, although I still don't know why the non-exported toolshed/1 can actually be called.

2
votes

You can make your code work with multifile predicates as well. [Update: this solves the loading conflict but without more changes both robin and arthur can use each other tools.] But the primary multifile predicate declaration (i.e. the one without an explicit module qualification) must be on the helper module:

:- module(helper, [helper/2]).

:- multifile(toolshed/1).

helper(friar_tuck,help) :-
   format("I will help you rout the Sheriff of Nottingham's men!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

helper(merlin,help) :-
   format("I will help you rout Mordred's army!~n"),
   setof(X,toolshed(X),Tools),
   format("I found these tools: ~q~n",[Tools]),
   format("Have at them!~n").

The arthur module becomes:

:- module(arthur, [arthur/0]).

:- use_module('helper.pl').

arthur :- helper(merlin,help).

:- multifile(helper:toolshed/1).
helper:toolshed('a slightly musty grimoire').
helper:toolshed('a jar of mandragore').
helper:toolshed('a fresh toadstool').

Similar for the robin module:

:- module(robin, [robin/0]).

:- use_module('helper.pl').

robin :- helper(friar_tuck,help).

:- multifile(helper:toolshed/1).
helper:toolshed('a katana made by mastersmith Masamune').
helper:toolshed('an ancient shovel with a sharpened blade').
helper:toolshed('an Uzi 9mm with Norinco markings').

Sample calls:

?- [helper, arthur, robin].
true.

?- arthur.
I will help you rout Mordred's army!
I found these tools: ['a fresh toadstool','a jar of mandragore','a katana made by mastersmith Masamune','a slightly musty grimoire','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!
true.

?- robin.
I will help you rout the Sheriff of Nottingham's men!
I found these tools: ['a fresh toadstool','a jar of mandragore','a katana made by mastersmith Masamune','a slightly musty grimoire','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!
true.

Btw, never write:

:- use_module(library('helper.pl')).

when you're loading another module that's in the same directory as the importing module. That's just begging for trouble as library is a directory alias with potentially multiple definitions (in an order that most likely you're not controlling) coupled with a search mechanism.

1
votes

An interesting problem. Follows a fully portable Logtalk solution (with minor changes for portability and avoiding dependencies on the pesky double_quotes flag; format/2 is a de facto standard predicate that accepts an atom in the first argument but the way the predicate is made available depends on the Prolog system and there's no need of cluttering the code with those details):

:- protocol(toolshed).

    :- public(toolshed/1).

:- end_protocol.


:- category(helper).

    :- private(helper/2).

    helper(friar_tuck, help) :-
        write('I will help you rout the Sheriff of Nottingham\'s men!\n'),
        setof(X, ::toolshed(X), Tools),
        write('I found these tools: '), writeq(Tools), nl,
        write('Have at them!\n').

    helper(merlin, help) :-
        write('I will help you rout Mordred\'s army!\n'),
        setof(X, ::toolshed(X), Tools),
        write('I found these tools: '), writeq(Tools), nl,
        write('Have at them!\n').

:- end_category.


:- object(robin,
    implements(toolshed),
    imports(helper)).

    :- public(robin_hood/0).

    robin_hood :-
        ^^helper(friar_tuck, help).

    toolshed('a katana made by mastersmith Masamune').
    toolshed('an ancient shovel with a sharpened blade').
    toolshed('an Uzi 9mm with Norinco markings').

:- end_object.


:- object(arthur,
    implements(toolshed),
    imports(helper)).

    :- public(arthur/0).

    arthur :-
        ^^helper(merlin, help).

    toolshed('a slightly musty grimoire').
    toolshed('a jar of mandragore').
    toolshed('a fresh toadstool').

:- end_object.

Then (assuming the above code saved in a dh.lgt file):

$ swilgt
...

?- {dh}.
...
true.

?- robin::robin_hood.
I will help you rout the Sheriff of Nottingham's men!
I found these tools: ['a katana made by mastersmith Masamune','an Uzi 9mm with Norinco markings','an ancient shovel with a sharpened blade']
Have at them!~n
true.

?- arthur::arthur.
I will help you rout Mordred's army!
I found these tools: ['a fresh toadstool','a jar of mandragore','a slightly musty grimoire']
Have at them!~n
true.

You can run this solution not only with Logtalk but with all its supported backend Prolog systems.

If you don't want the toolshed/1 predicate to be public, you can change the object opening directives to make the predicate either protected or private. For example:

:- object(robin,
    implements(private::toolshed),
    imports(helper)).

This will give you:

?- arthur::toolshed(T).
!     Permission error: access private_predicate toolshed/1
!       in goal: arthur::toolshed(A)

You could also change the protocol to make the predicate private there but that would not be idiomatic.