This is the first post of a serie about half-broken hacks claimed to provide an alternative to Logtalk features. Half-broken means that, although the hack appears to provide a sought after feature on a cursory glance, close examination quickly uncovers limitations, flaws, and corner cases where it fails to provide the desired functionality and semantics.
In the particular case of the reexport/1
directive, the claim is that
it provides an implementation of inheritance. A specification for this
directive can be found in the ISO standard for Prolog modules:
6.2.4.4 Module interface directive reexport/1
A module interface directive
reexport(PI)
in the module interface of a moduleM
, wherePI
is an atom, a sequence of atoms, or a list of atoms specifies that the moduleM
imports all the user defined procedures exported or re-exported by the modules designated byPI
and thatM
makes these procedures available for import into or re-exportation by other modules.
Although this standard is basically ignored (for good reasons) by implementers, the directive itself is found in some Prolog module systems such as Ciao, ECLiPSe, SWI-Prolog and YAP. It’s absent, however, from systems such as SICStus Prolog and XSB.
As a simple example of an attempt to use the reexport/1
directive as an
implementation of inheritance, consider the following three modules, each
defined in a source file named after the module:
:- module(m1, [d/1, s/1]).
:- dynamic(d/1).
d(m1).
s(m1).
:- module(m2, []).
:- reexport(m1).
s(m2).
:- module(m3, []).
:- reexport(m2).
The first issue we find is in loading the source files. Due to the predicate
import/export semantics, simply consulting the files either results in an
error (e.g. SWI-Prolog) or in the redefinition of imported predicates in
user
space (e.g. YAP). To avoid this issue, we can instead use the
use_module/2
directive with an empty import list. Let’s use SWI-Prolog
for the sample queries.
?- use_module(m1,[]), use_module(m2,[]), use_module(m3,[]).
Warning: m2.pl:6:
Warning: Local definition of m2:s/1 overrides weak import from m1
true.
The warning we get is due to the “inherited” definition for the s/1
predicate from module m1
, which is redefined in the module m2
. As
the module m3
simply reexports the module m2
, which in turn reexports
module m1
(claimed to form an “inheritance” chain), let’s query it:
?- m3:s(X).
X = m2.
?- m3:d(X).
X = m1.
So far, so good. We inherit the defintion of s/1
from m2
(which
overrides the definition inherited from m1
) and the defintion of
d/1
from m1
. Let’s now try to override the definition of d/1
in m2
:
?- m2:assertz(d(m2)).
true.
?- m3:d(X).
X = m1 ;
X = m2.
We get a different behaviour for the d/1
dynamic predicate compared
with the s/1
static predicate. Worse, the first solution we get for
d/1
is not from the module close to m3
in the “inheritance” chain,
which is m2
, but from the m1
. Not what we expect from a proper
implementation of inheritance. Let’s undo the change by retracting all
clauses for d/1
in the module m2
:
?- m2:retractall(d(_)).
true.
Retrying the m3:d/1
query:
?- m3:d(X).
false.
Oops! we now lost both the defintion from m2
, as expected, but also
the definition from m1
! Let’s query m1
to try to understand what’s
happening:
?- m1:d(X).
false.
The d1
definition is also gone from m1
although the retractall/1
goal was executed in the context of m2
.
Thus, not only reexport/1
gives different “inheritance” semantics
for static and dynamic predicates, which is wrong as the changeable
property of a predicate is orthogonal to inheritance, but also the
standard database built-in predicates fail to provide the expected
semantics.
A fair question at this point, given the lack of standardization of Prolog modules, is if we are observing here a behaviour specific to SWI-Prolog. Let’s try the same sequence of queries on YAP:
?- use_module(m1,[]), use_module(m2,[]), use_module(m3,[]).
reconsulting m1...
reconsulted m1.pl in module m1, 1 msec 8144 bytes
reconsulting m2...
m2.pl:6: Module m2 redefines imported predicate m1:s/1.
reconsulted m2.pl in module m2, 1 msec 1352 bytes
reconsulting m3...
reconsulted m3.pl in module m3, 0 msec 1472 bytes
true
?- m3:s(X).
X = m2 ? ;
false.
?- m3:d(X).
X = m1 ? ;
false.
?- m2:assertz(d(m2)).
true
?- m3:d(X).
X = m2 ? ;
false.
?- m2:retractall(d(_)).
true ? ;
false.
?- m3:d(X).
false.
?- m1:d(X).
X = m1 ? ;
false.
In this case, the redefinition of d/1
in module m2
appears to
work but retracting it doesn’t returns to the previous state as
m3:d/1
stops finding the solution inherited from m1
. Thus, broken
results as well from an inheritance perspective.
Would ECLiPSe do better here? No. Attempting to load the ECLiPSe versions of the modules above results in a compilation error:
Stream :6:
trying to redefine an existing imported procedure in s / 1
Error(s) occurred while compiling /Users/pmoura/rex/m2.ecl
Aborting execution ...
Abort
From the ECLiPSe documentation on the reexport/1
directive:
Reexporting is not compatible with a local definition of the same name (because reexport always implies an import as well), it raises error 92.
The documentation suggests a workaround, which results in the following
updated version of the module m2
:
:- module(m2).
:- reexport m1 except s/1.
:- export s/1.
s(m2).
Trying our sequence of queries:
[eclipse 7]: ensure_loaded(m1), ensure_loaded(m2), ensure_loaded(m3).
source_processor.eco loaded in 0.00 seconds
hash.eco loaded in 0.00 seconds
compiler_common.eco loaded in 0.01 seconds
compiler_normalise.eco loaded in 0.00 seconds
compiler_map.eco loaded in 0.00 seconds
compiler_analysis.eco loaded in 0.00 seconds
compiler_peephole.eco loaded in 0.01 seconds
compiler_codegen.eco loaded in 0.01 seconds
compiler_varclass.eco loaded in 0.00 seconds
compiler_indexing.eco loaded in 0.00 seconds
compiler_regassign.eco loaded in 0.00 seconds
asm.eco loaded in 0.01 seconds
module_options.eco loaded in 0.00 seconds
ecl_compiler.eco loaded in 0.07 seconds
m1.ecl compiled 40 bytes in 0.00 seconds
m2.ecl compiled 40 bytes in 0.00 seconds
m3.ecl compiled 0 bytes in 0.00 seconds
Yes (0.07s cpu)
[eclipse 8]: m3:s(X).
X = m2
Yes (0.00s cpu)
[eclipse 9]: m3:d(X).
X = m1
Yes (0.00s cpu)
[eclipse 10]: assertz(d(m2))@m2.
trying to redefine an existing imported procedure in assertz(d(m2))
Abort
Thus, in the case of ECLiPSe, we cannot override an “inherited” definition dynamically at runtime.
It’s clear from this simple experiment that, although the reexport/1
directive may provide an approximation to inheritance semantics in
limited and controlled setups where portability is not a requirement,
it fails to be a general solution. Are the issues exposed above the
result of bugs that could be fixed? A bug is a deviation from a specification
that formalizes correct behaviour. But there’s no mention of inheritance or
inheritance semantics in the standard’s specification of either the
directive or the database predicates. Moreover, any “fix” will
need to preserve existing semantics for the directive and predicates
when not being used to try to mimic inheritance.
Concluding, there’s a world of difference between a language designed from the ground up to provide features such as inheritance, as exemplified by Logtalk, and twisting Prolog constructs to try to provide features that were never part of their design and specification.
P.S. For completeness, follows the Logtalk version of the test modules and the sample queries:
:- object(m1).
:- public([d/1, s/1]).
:- dynamic(d/1).
d(m1).
s(m1).
:- end_object.
:- object(m2,
extends(m1)).
s(m2).
:- end_object.
:- object(m3,
extends(m2)).
:- end_object.
Sample queries:
?- {m1, m2, m3}.
% [ m1.lgt loaded ]
% (0 warnings)
% [ m2.lgt loaded ]
% (0 warnings)
% [ m3.lgt loaded ]
% (0 warnings)
true.
?- m3::s(X).
X = m2.
?- m3::d(X).
X = m1.
?- m2::assertz(d(m2)).
true.
?- m3::d(X).
X = m2.
?- m2::retractall(d(_)).
true.
?- m3::d(X).
X = m1.
?- m1::d(X).
X = m1.
The Logtalk version is, of course, fully portable. You can use any of the Logtalk supported backend Prolog compilers to run it.