The inheritance mechanisms found on object-oriented programming languages allow the specialization of previously defined objects, avoiding the unnecessary repetition of code and allowing the definition of common functionality for sets of objects. In the context of logic programming, we can interpret inheritance as a form of theory extension: an object will virtually contain, besides its own predicates, all the predicates inherited from other objects that are not redefined locally. Inheritance is not, however, the only mechanism for theory extension. Logtalk also supports composition using categories.
Logtalk uses a depth-first lookup procedure for finding predicate declarations and predicate definitions, as explained below, when a message is sent to an object. The lookup procedures locate the entity holding the predicate declaration and the entity holding the predicate definition using the predicate name and arity. The alias/2 predicate directive may be used for defining alternative names for inherited predicates, for solving inheritance conflicts, and for giving access to all inherited definitions (thus overriding the default lookup procedure).
The lookup procedures are used when sending a message (using the (::)/2, (::)/1, and /1 control constructs) and when making super calls (using the (^^)/1 control construct). The exact details of the lookup procedures depend on the role played by the object receiving the message or making the super call, as explained next. The lookup procedures are also used by the current_predicate/1 and predicate_property/2 reflection predicates.
Protocol inheritance refers to the inheritance of predicate declarations (scope directives). These can be contained in objects, protocols, or categories. Logtalk supports single and multi-inheritance of protocols: an object or a category may implement several protocols and a protocol may extend several protocols.
Lookup order for prototype hierarchies
The lookup order for predicate declarations is first the object, second the implemented protocols (and the protocols that these may extend), third the imported categories (and the protocols that they may implement), and finally the objects that the object extends (following their declaration order). This lookup is performed in depth-first order. When an object inherits two different declarations for the same predicate, by default, only the first one will be considered.
Lookup order for class hierarchies
The lookup order for predicate declarations is first the object classes (following their declaration order), second the classes implemented protocols (and the protocols that these may extend), third the classes imported categories (and the protocols that they may implement), and finally the superclasses of the object classes. This lookup is performed in depth-first order. If the object inherits two different declarations for the same predicate, by default, only the first one will be considered.
Implementation inheritance refers to the inheritance of predicate definitions. These can be contained in objects or in categories. Logtalk supports multi-inheritance of implementation: an object may import several categories or extend, specialize, or instantiate several objects.
Lookup order for prototype hierarchies
The lookup order for predicate definitions is similar to the lookup for predicate declarations except that implemented protocols are ignored (as they can only contain predicate directives).
Lookup order for class hierarchies
The lookup order for predicate definitions is similar to the lookup for predicate declarations except that implemented protocols are ignored (as they can only contain predicate directives) and that the lookup starts at the instance itself (that received the message) before proceeding, if no predicate definition is found there, to the instance classes imported categories and then to the class superclasses.
Redefining inherited predicate definitions
When we define a predicate that is already inherited from an ancestor object or an imported category, the inherited definition is hidden by the new definition. This is called inheritance overriding: a local definition overrides any inherited definitions. For example, assume that we have the following two objects:
:- object(root). :- public(bar/1). bar(root). :- public(foo/1). foo(root). :- end_object. :- object(descendant, extends(root)). foo(descendant). :- end_object.
After compiling and loading these objects, we can check the overriding behavior by trying the following queries:
| ?- root::(bar(Bar), foo(Foo)). Bar = root Foo = root yes | ?- descendant::(bar(Bar), foo(Foo)). Bar = root Foo = descendant yes
However, we can explicitly code other behaviors. Some examples follow.
Specializing inherited predicate definitions
Specialization of inherited definitions: the new definition calls the
inherited definition and makes additional calls. This is accomplished
by calling the (^^)/1 super call operator
in the new definition. For example, assume a
that must account for object specific initializations along the
:- object(root). :- public(init/0). init :- write('root init'), nl. :- end_object. :- object(descendant, extends(root)). init :- write('descendant init'), nl, ^^init. :- end_object.
| ?- descendant::init. descendant init root init yes
Union of inherited and local predicate definitions
Union of the new with the inherited definitions: all the definitions are taken into account, the calling order being defined by the inheritance mechanisms. This can be accomplished by writing a clause that just calls, using the (^^)/1 super call operator, the inherited definitions. The relative position of this clause among the other definition clauses sets the calling order for the local and inherited definitions. For example:
:- object(root). :- public(foo/1). foo(1). foo(2). :- end_object. :- object(descendant, extends(root)). foo(3). foo(Foo) :- ^^foo(Foo). :- end_object.
| ?- descendant::foo(Foo). Foo = 3 ; Foo = 1 ; Foo = 2 ; no
Selective inheritance of predicate definitions
The selective inheritance of predicate definitions (also known as differential inheritance) is normally used in the representation of exceptions to inherited default definitions. We can use the (^^)/1 super call operator to test and possibly reject some of the inherited definitions. A common example is representing flightless birds:
:- object(bird). :- public(mode/1). mode(walks). mode(flies). :- end_object. :- object(penguin, extends(bird)). mode(swims). mode(Mode) :- ^^mode(Mode), Mode \== flies. :- end_object.
| ?- penguin::mode(Mode). Mode = swims ; Mode = walks ; no
Public, protected, and private inheritance
To make all public predicates declared via implemented protocols, imported categories, or ancestor objects protected predicates or to make all public and protected predicates private predicates we prefix the entity’s name with the corresponding keyword. For example:
:- object(Object, implements(private::Protocol)). % all the Protocol public and protected % predicates become private predicates % for the Object clients ... :- end_object.
:- object(Class, specializes(protected::Superclass)). % all the Superclass public predicates become % protected predicates for the Class clients ... :- end_object.
Omitting the scope keyword is equivalent to using the public scope keyword. For example:
:- object(Object, imports(public::Category)). ... :- end_object.
This is the same as:
:- object(Object, imports(Category)). ... :- end_object.
This way we ensure backward compatibility with older Logtalk versions and a simplified syntax when protected or private inheritance are not used.
Logtalk supports multi-inheritance by enabling an object to extend, instantiate, or specialize more than one object. Likewise, a protocol may extends multiple protocols and a category may extend multiple categories. In this case, the depth-first lookup algorithms described above traverse the list of entities per relation from left to right. Consider as an example the following object opening directive:
:- object(foo, extends((bar, baz))).
The lookup procedure will look first into the parent object bar and its related entities before looking into the parent object baz. The alias/2 predicate directive can always be used to solve multi-inheritance conflicts. It should also be noted that the multi-inheritance support does not affect performance when we use single-inheritance.
Composition versus multiple inheritance
It is not possible to discuss inheritance mechanisms without referring to the long and probably endless debate on single versus multiple inheritance. The single inheritance mechanism can be implemented efficiently but it imposes several limitations on reusing, even if the multiple characteristics we intend to inherit are orthogonal. On the other hand, the multiple inheritance mechanisms are attractive in their apparent capability of modeling complex situations. However, they include a potential for conflict between inherited definitions whose variety does not allow a single and satisfactory solution for all the cases.
No solution that we might consider satisfactory for all the problems presented by the multiple inheritance mechanisms has been found. From the simplicity of some extensions that use the Prolog search strategy like [McCabe92] or [Moss94] and to the sophisticated algorithms of CLOS [Bobrow_et_al_88], there is no adequate solution for all the situations. Besides, the use of multiple inheritance carries some complex problems in the domain of software engineering, particularly in the reuse and maintenance of the applications. All these problems are substantially reduced if we preferably use in our software development composition mechanisms instead of specialization mechanisms [Taenzer89]. Multiple inheritance is best used as an analysis and project abstraction, rather than as an implementation technique [Shan_et_al_93]. Note that Logtalk provides first-class support for composition using categories.