This is the third 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 protocols (or interfaces), the claim is that they can emulated using the include/1
directive. As a hack, this is a relative benign one but still with significant limitations. A specification for this directive can be found in the ISO Prolog Core standard:
7.4.2.7
include/1
If
F
is an implementation defined ground term designating a Prolog text unit, then Prolog textP1
which contains a directiveinclude(F)
is identical to a Prolog textP2
obtained by replacing the directiveinclude(F)
inP1
by the Prolog text denoted byF
.
As the specification makes clear, this directive provides textual inclusion of a file in another file (the de facto translation of “text unit”). As an example, assume a common.pl
file containing the following export/1
directive:
:- export(sound/0).
The export/1
directive is in turn specified in ISO standard for Prolog modules:
6.2.4.2 Module interface directive export/1
A module interface directive
export(PI)
in the module interface of a moduleM
, wherePI
is a predicate indicator, a predicate indicator sequence or a predicate indicator list, specifies that the moduleM
makes the procedures designated byPI
available for import into or re-export by other modules.A procedure designated by
PI
in aexport(PI)
directive shall be that of a procedure defined in the body (or bodies) of the moduleM
.No procedure designated by
PI
shall be a control construct, a built-in predicate, or an imported procedure.
You likely noticed the word “interface” above in the specification of the directive. Indeed, the ISO standard for Prolog modules also specifies module interfaces. But it does so by only allowing a single implementation for the interface where both share the module name. Given that the essential characteristic of an interface (or protocol) is to allow multiple implementations, no point in wasting time with the useless (and ignored by implementers) concept of interface in the standard. But the export/1
directive itself is found in some Prolog systems such as ECLiPSe, SWI-Prolog, XSB, and YAP (but not implemented in e.g. SICStus Prolog).
But back to attempting to use the include/1
directive to share predicate export directives between modules. Using the common.pl
file, we can define the following modules:
:- module(dog, []).
:- include(common).
sound :-
write('Woof...'), nl.
:- module(cat, []).
:- include(common).
sound :-
write('Meowww...'), nl.
Given that the two modules export the same predicate, we need to load in a way that avoid name clashes. For example, using SWI-Prolog:
?- use_module(dog, []), use_module(cat, []).
true.
Consequently, we also need to use explicit-qualified calls to the sound/0
predicate:
?- dog:sound.
Woof...
true.
?- cat:sound.
Meowww...
true.
As illustrated in this simple example, the include/1
does work as we expected when calling the exported predicate. But a file is not an interface (or protocol). Modules and files are not at the same abstraction level. Moreover, given the textual inclusion semantics, and from the perspective of a reflection API, instead of several modules implementing the same interface, we have modules that happen to export the same (subset of) predicates. There’s no concept of interface (or protocol) as an entity at the same abstraction level as modules that we can query, document, or assign a version tag. We cannot make a simple query to find which modules implement a given interface. The textual inclusion also results in a small space overhead as the directives are effectively duplicated in each module that includes a file but that’s a minor issue.
There is also a more general problem with the implementation of interfaces (or protocols) in a module system that is not related to the include/1
directive per se but to the lack of a clear distinction between declaring a predicate and defining a predicate in Prolog. Recall the specification quoted above of the export/1
directive:
A procedure designated by
PI
in aexport(PI)
directive shall be that of a procedure defined in the body (or bodies) of the moduleM
.
Let’s update the included file, common.pl
, with a second export/1
directive:
:- export(sound/0).
:- export(fly/0).
Without also updating the dog
and cat
modules, the predicate fly/0
is now declared (that’s the essence of an interface or protocol) but not defined. But if we try again to load the modules we now get:
?- use_module(dog, []), use_module(cat, []).
ERROR: Exported procedure dog:fly/0 is not defined
ERROR: Exported procedure cat:fly/0 is not defined
true.
I.e. closed-world assumption (CWA) semantics doesn’t work for predicates that are declared but not defined. More precisely, CWA doesn’t work for declared static predicates that are not defined. What about dynamic predicates? Let’s update the common.pl
file to:
:- export(sound/0).
:- export(fly/0).
:- dynamic(fly/0).
We can now load the dog
and cat
modules without errors and get CWA semantics for the fly/0
dynamic, exported, predicate. For example:
?- dog:fly.
false.
Is the lack of CWA semantics for static predicates a significant issue? Predicates are the building block of Prolog
applications. That includes knowledge representation. Representing e.g. a attribute that can be true or false is orthogonal to that attribute being immutable (static) or changeable (dynamic). Of course, in our example above, we can keep the fly/0
predicate as exported and static by using the ugly workaround of adding the following clause to any module where calling the predicate should fail:
fly :-
fail.
In contrast, Logtalk provides a clean design and implementation of protocols (interfaces). Protocols are first-class entities (like objects and categories) and can thus be defined, documented, versioned, and queried (using Logtalk reflection API). Logtalk also provides a clear distinction between declaring a predicate and defining a predicate and CWA semantics for all declared predicates, including static predicates.
P.S. For completeness, follows the Logtalk version of the example above. We start by defining the common
protocol and the doc
and cat
objects implementing the protocol:
:- protocol(common).
:- public([sound/0, fly/0]).
:- end_protocol.
:- object(dog, implements(common)).
sound :-
write('Woof...'), nl.
:- end_object.
:- object(cat, implements(common)).
sound :-
write('Meowww...'), nl.
:- end_object.
Loading (assuming each entity saved to its own file named after the entity) and sample queries:
?- {common, doc, cat}.
...
true.
?- dog::sound.
Woof...
true.
?- cat::sound.
Meowww...
true.
?- dog::fly.
false.
?- current_protocol(common).
true.
?- protocol_property(common, public(Predicates)).
Predicates = [sound/0, fly/0].