Module loading

Here is something unexpected about Modules:

Suppose you have two modules:

In file1.pl:

:-module('com.example.megacorp', [make_money/2]).

make_money("Make more money!", "Yes, sir").

In file2.pl:

:-module('com.example.megacorp.hq', [make_money/2]).

make_money("Make more money!", "Yes, sir").

Note that the module names differ, but the exported predicates have the same descriptor (using correct ISO terminology, the exported procedures have the same predicate indicator, but we know what we mean)

Then, in the REPL:

?- [file1].
true.

?- [file2].
ERROR: import/1: No permission to import 'com.example.megacorp.hq':make_money/2 into user (already imported from 'com.example.megacorp')
true.

Okay. But now:

In file1.pl, unchanged:

:-module('com.example.megacorp', [make_money/2]).

make_money("Make more money!", "Yes, sir").

In file2.pl, I leave the list of publicly visible predicates as it was (erroneously) and just change the name of the defined procedure:

:-module('com.example.megacorp.hq', [make_money/2]).

make_money_2("Make more money!", "Yes, sir").

Unexpectedly, both files can be loaded.

?- [file1].
true.

?- [file2].
true.

I didn’t expect that. Why is this possible?

1 Like

Try (with the second version of your modules):

?- [file1].
true.

?- [file2].
true.

?- predicate_property('com.example.megacorp.hq':make_money(_, _), imported_from(M)).
M = 'com.example.megacorp'.

Art this point, you would possibly reply that the second module doesn’t import the first module. True. But when loading the first module, it exports the make_money/2 predicate to module user (which is the file loading context). As (SWI-Prolog) modules inherited predicates by default from user, you get no errors when loading the second file and you can use queries such as:

?- 'com.example.megacorp.hq':make_money(A, B).
A = "Make more money!",
B = "Yes, sir".

And now, exhibiting great restraint, I will end my explanation with no judgement passed on Prolog modules.

3 Likes

Disclaimer: From what I know about modules, I have concluded that we cannot use modules as they are at the moment for object-oriented programming with polymorphism.

There is something I need to ask first. Why make modules and then use

?- [filename].

instead of

?- use_module(module_name).

… and why the com.something.somethingelse.omg module names?

Here is what I get with three modules:

$ cat m1.pl
:- module(m1, [foo/1]).

foo('This is in module m1').
$ cat m2.pl
:- module(m2, [foo/1]).

foo('This is in the m2 module').
$ cat m3.pl
:- module(m3, [foo/1]).

bar('I made a mistake').

?- use_module(m1).
true.

?- use_module(m2).
ERROR: import/1: No permission to import m2:foo/1 into user (already imported from m1)
true.

?- foo(X).
X = 'This is in module m1'.

?- m2:foo(X).
X = 'This is in the m2 module'.

This so far makes sense, intuitively, and doesn’t feel misleading.

The issue that @pmoura showed is indeed bad. Here it is, a bit more obvious to my eyes at least.

First, still OK – load a module that has a programmer’s mistake in it:

$ swipl -q
?- use_module(m3).
ERROR: Exported procedure m3:foo/1 is not defined
true.

Now, quite bad – load first a module that is fine, than the module that has a mistake in it:

$ swipl -q
?- use_module(m1).
true.

?- use_module(m3).
true. % Hmm....

?- foo(X).
X = 'This is in module m1'.

?- m3:foo(X).
X = 'This is in module m1'. % Oh noes
  1. Hmm … not sure. I just wanted to get the modules into the runtime. Is there is a difference? There better not be.
  2. The SWI Prolog manual says Modules are organised in a single and flat namespace and therefore module names must be chosen with some care to avoid conflicts. This problem exists also in Java, and Java’s convention for avoiding package name clashes – using the reverse domain name of the issuing organization, suitably suffixed --, immediately presents itself as solution. I just retained this in this example.

(About which, for people not au fait with the Java ecosystem: There is the Java package conventions from the 90s, the OSGi Module system, a complete module management system and there is the Java (Platform) Module system, used mostly for modularizing the JDK.)

I don’t know for sure. I guess we’d have to RTFM to figure it out :-). I always assumed there must be a difference, since you write the two things differently. Why have two different things if they are identical?

I might be wrong about this, but the underlying issue that Java package naming solved stems from not distributing source code? Or maybe it has something to do with “everything must be a class in its own class file” straight-jacket?

Modules are objects (see e.g. Modules are objects - Logtalk). Just poorly designed as an object system (which was never the intent). See e.g. Blog - Logtalk for some of the main problems in using modules systems as full fledge objects systems.

Indeed you can get a compilation error or a successful loading depending just on the file loading order (of two **independent ** modules).

2 Likes

I don’t know for sure. I guess we’d have to RTFM to figure it out :-). I always assumed there must be a difference, since you write the two things differently. Why have two different things if they are identical?

Well, generally it is desire for backward compatibility. One wants to change the functionality in a rational way, or even dump some of it, but that’s not possible because of that one company that lost its source code in the great harddisk crash of '77 and people like the old way or it’s faster to type. At best, you then have different ways of doing, at worst your system is getting increasingly paraconsistent (“don’t call X before Y or all hell breaks loose”).

I might be wrong about this, but the underlying issue that Java package naming solved stems from not distributing source code?

No, it was just about how to name packages so that no two origins exist for a package named for example “logging”. The great thing is that you don’t need the source to discover the API (but the JavaDoc is really useful), but if a package name clash occurs, there is trouble (that can be solved by creating a separate Classloader instance as I remember, but this is ugly).

Correct

The user module plays a special role for mostly historical reasons, but also to facilitate unstructured programming, which has its advantages for code that is more or less intended for single use. If you do not want this, use a module for every part of your code and do not use user, with a possible exception of making the entry point of your program. I hardly ever write non-module code, except for very quick tests or single use programs.

3 Likes

The only difference between [file] and use_module(file) is that the latter raises an error if the file is not a module. I’d always use use_module in code, but at the toplevel [file] is quicker to type.

Sorry, use_module only loads the file if it is not loaded. [file] _reloads_the file if it is loaded. Tyoically not needed in SWI as there is make/0.

1 Like

But the issue here is that user is used by default. People use modules to partitioning their code and often are oblivious that their code only works (instead of getting compilation errors) because of the default inheritance from the user module and some luck in compilation/loading order. This is not a theoretical problem and this issue is easy to find in the public Prolog codebases.

OK, so just to check if I understand what you are saying. Instead of loading from the top level, I made another module that uses the two (conflicting) modules. It now looks much better. Here is the content of the three modules:

$ cat m1.pl
:- module(m1, [foo/1]).

foo('This is in module m1').
$ cat m3.pl
:- module(m3, [foo/1]).

bar('I made a mistake').
$ cat top.pl
:- module(top, [m1_foo/1, m3_foo/1]).

:- use_module(m1).
:- use_module(m3).

m1_foo(X) :-
    foo(X).

m3_foo(X) :-
    m3:foo(X).

And here is what happens when I try to use the top module:

?- use_module(top).
ERROR: /home/boris/code/own/prolog/modules/top.pl:4:
ERROR:    Exported procedure m3:foo/1 is not defined
ERROR: /home/boris/code/own/prolog/modules/top.pl:4:
ERROR:    import/1: No permission to import m3:foo/1 into top (already imported from m1)
true.

?- m1_foo(X).
X = 'This is in module m1'.

?- m3_foo(X).
ERROR: Unknown procedure: m3:foo/1
ERROR: In:
ERROR:   [11] m3:foo(_22348)
ERROR:   [10] top:m3_foo(_22378) at /home/boris/code/own/prolog/modules/top.pl:10
ERROR:    [9] <user>
   Exception: (11) m3:foo(_15152) ? creep
?- m3:bar(X).
X = 'I made a mistake'.

Actually, your example also illustrates the danger of use_module/1 directives. Consider the following variant of your modules:

$ cat m1.pl
:- module(m1, [foo/1]).

foo('This is in module m1').
$ cat m2.pl
:- module(m2, [bar/1]).

bar('This is in module m2').
$ cat top.pl
:- module(top, [p/0]).

:- use_module(m1).
:- use_module(m2).

p :- foo(_), bar(_).

You load the top module. Everything is fine. Later module m2 is update to:

:- module(m2, [foo/1, bar/1]).

foo('I am new here!').
bar('This is in module m2').

Your application no longer compiles (generating the error that you illustrated) despite the fact then, when you wrote it, you intended to take foo/1 from m1. Problem is, the m1:foo/1 dependency is implicit. Same for the m2:bar/1 dependency. This makes any dependency updates potentially hazardous. Not something that should keep people awake at night but definitely something that people should be aware.

2 Likes

True. Modules are not objects/classes though and where methods tend to be short and not carrying a clue on the data processed, predicates tend to carry a hint on the data or family they belong to such as http_open/3. There are of course the historical exceptions (yes, transpose/2 is one). Using these conventions it should be practically impossible to get into a conflict. If you do not want to use longer names you can use use_module/2. Note that that may imply having to use module:goal, which IMO is a rather bad idea. For one thing, it makes tracking the dependencies a lot harder. Also, module names should be fairly long to avoid conflicts while an indication of where the predicate belongs to can be much shorter as conflicts only arise where different contexts are loaded into the same module and have similar operations.

I still mostly disagree the module system is badly broken. Only, you should use it as intended and one could probably claim tutorials on that are note spread widely enough.

2 Likes

Modules are objects. They are not classes, however, but prototypes (OO doesn’t imply classes; anyone thinking that have a lot to learn about OO). Here:

Indeed following (naming) conventions goes a long way in avoiding conflicts. That, however, doesn’t imply that those conventions are a best practice by itself. E.g. reusing names simplify learning APIs and make them consistent.

There is no reason for it to be harder other than current limitations on the (SWI-Prolog) tools or the (SWI-Prolog) reflection API support. E.g. Logtalk tools and reflection API have no issue tracking module dependencies independently of implicit or explicit qualification.

A classic case of Stockholm syndrome. After +20 years of sharing alternative and elegant solutions based on constructs actually designed to tackle software engineering issues and requirements, the most common reaction I get from Prolog implementers/vendors (not users, implementers/vendors) is silence or hostility or half-broken ugly hacks that attempt to the same within the module systems.