ciao-lang/ciao

order in predicate visibility rules

jfmc opened this issue · 10 comments

jfmc commented

Note that predicates are never overridden, so the problem may be in the predicate visibility rules.

:- module(a, [foo/1]).
foo(a).
:- module(b, [foo/1]).
foo(b).
?- use_module(a).

yes
?- foo(X).

X = a ? 

yes
?- use_module(b).

yes
?- foo(X).

X = a ? 

yes

As @mherme says both a:foo(X) and b:foo(X) work, but module visibility rules in Ciao resolves foo(X) as a:foo(X). Perhaps b:foo(X) would be more natural? Changing it is not trivial but can do it, specially if this is expected in other languages.

Originally posted by @jfmc in #61 (comment)

SWI-Prolog throws an error:

% a.pl compiled into a 0.00 sec, 1 clauses
ERROR: [Thread pce] import/1: No permission to import b:foo/1 into user (already imported from a)
% b.pl compiled into b 0.02 sec, 1 clauses

On the other hand SWI-Prolog allows this overriding:

:- module(c, [foo/1]).
:- use_module(a).
foo(c).

But a little annoyingly it gives a warning:

%  a compiled into a 0.00 sec, 1 clauses
Warning: c:/users/rburs/desktop/rr/c.pl:3:
Warning:    Local definition of c:foo/1 overrides weak import from a
% c.pl compiled into c 0.00 sec, 2 clauses
?- foo(X).
X = c.

You can switch off the warning, by using use_module/2 with except/1:

:- module(c, [foo/1]).
:- use_module(a, except([foo/1])).
foo(c).

Now the warning goes away:

%  a compiled into a 0.00 sec, 1 clauses
% c.pl compiled into c 0.00 sec, 2 clauses
?- foo(X).
X = c.
jfmc commented

Actually I was interested in the behavior of other languages:
a.py:

def foo():
  return "a"

b.py:

def foo():
  return "b"

c.py:

from a import * 
from b import * 
print(foo())
$ python c.py
b

(if I'm not confused) Python resolves names in LIFO while Ciao does in FIFO. Rather than errors, we show warnings if there are conflicts. Python is silent. SWI gives errors. JavaScript ES6 modules forbid 'import *' to avoid problems.

At least for the Ciao toplevel it would make sense to adopt LIFO.

What does the ISO module standard say?

Unbenannt2

But I agree there are alternatives around, being more
tollerant. What some Programming languages then use
is for example this:

C3 linearization
https://en.wikipedia.org/wiki/C3_linearization

You might not see it in the simple example you are dealing with.

(if I'm not confused) Python resolves names in LIFO while Ciao does in FIFO. Rather than errors, we show warnings if there are conflicts. Python is silent. SWI gives errors. JavaScript ES6 modules forbid 'import *' to avoid problems.

JS ES6, like Logtalk, is the only one in your list above that provides a sensible solution. For Prolog, that means deprecating/killing the use_module/1 directive (or, as in Logtalk, re-purpose it only for declaring module aliases).

jfmc commented

Thanks @pmoura! I've checked your documentation and everything looks really reasonable. Something that we really miss in Ciao is a use_module/1 that enforces module qualification. We are not sure about killing use_module/1 (sometimes it is useful at least for legacy code or in controlled places). We had in the TODO list but never though about a syntax to write it. It is good to know that this is (more or less) consistent with your design in Logtalk.

Is there any doc/paper that explains it in more detail (e.g., describing other languages with the same decision)?

As you may have noticed in the documentation, Logtalk's use_module/1 directive takes a list as argument. That provides a simply solution to distinguish it during compilation from the Prolog use_module/1 directive.

The dangers of directives like the Prolog use_module/1 directive are well know and I have seen it discussed in several programming languages resources. One of the main problems with "just import everything" directives is that a working application today can easily become a broken application tomorrow when new predicates are added to a library that, necessarily, the application doesn't use but that cause a conflict (if you're lucky) with other libraries (which may be private to the application and thus invisible to the developers updating the libraries).

I agree. On the other hand many of those languages are meant only for building large applications, and not for prototyping. Prolog in its original form is really good for prototyping, and in its more modern incarnations (with modules, etc.) it is really great in that it allows traveling a smooth path from prototyping to production-strength code. Ciao in particular provides a lot of things to help this transition. For example you can write:

:- module(_,_).

which exports everything and takes as module name the name of the file. This is as flexible for prototyping as a user file, but it is scoped, i.e., you know all the code for this module is right here, and cannot be loaded in another place (as happens with at least traditional user files). This is very useful for detecting errors (undefined predicates are just an example), so that it actually improves prototyping. Getting back to use_module, being able to 'import everything' is admittedly more dangerous than not allowing it, but also much more flexible, and it is part of this ability to travel the path from prototyping to production-strength.

jfmc commented

Thanks @mherme and @pmoura! Let me summarize:

  • use_module/1 is dangerous (many of us rediscovered it independently)
  • Logtalk design, like other languages including JS ES6, encourages good practices (import everything is fine, but only if you are forced to qualify explicitly <- we'd like to add that to Ciao)
  • use_module/1 is still useful in some contexts (Ciao warns when there are potential issues)
  • LIFO order seems more reasonable at least in the toplevel.

Note that you can always do as in Logtalk: if the use_module/1 directive argument is a list, then it's declaring module aliases or modules that are being imported but require the use of explicit qualification. If not, it's the current/traditional use_module/1 directive.