Inheriting Multiple and Repeated Parts in Timor
J. Leslie Keedy, Christian Heinlein and Gisela
Menger, University of
Ulm, Germany
|
 |
REFEREED
ARTICLE

PDF Version |
Abstract
The paper describes one aspect of multiple inheritance in the Timor
programming language, viz. how "parts" such as a type Radio
and a type Cassette Player can be inherited, where appropriate repeatedly,
in subtypes such as a Radio Double Cassette Player. Because such types
can also be defined via aggregation the paper begins by comparing inheritance
with aggregation. It then shows how such cases can be handled first
at the type level and then at the implementation level in Timor.
1 INTRODUCTION
Multiple inheritance can be used to model a number of different situations.
Some cases involve a common abstract ancestor (e.g. variants of a type
Collection, such as Set, Bag or List) or a common concrete ancestor
(e.g. variants of a type Person with multiple roles, such as a type
EmployedStudent), while others involve inheritance of separate "parts" (e.g.
a type RadioCassettePlayer inheriting from a type Radio and a type
CassettePlayer). Sometimes repeated inheritance can be appropriate
(e.g. a DoubleDegreeStudent or a DoubleCassettePlayer), and repeated
inheritance can be combined with other forms of multiple inheritance
(e.g. DoublyEmployedStudent, RadioDoubleCassettePlayer). The situation
can be further complicated by the fact that the behaviour of some methods
of one or more base types might need to be redefined, and also one
or more of the types might have different implementations. Given this
multiplicity of aims it is perhaps not surprising that not all OO languages
provide mechanisms which adequately cover all the requirements.
In this
paper we describe the mechanisms which handle "parts" inheritance
provided in the language Timor1.
Timor is a new OO language currently being developed at the University
of Ulm with the primary aim of facilitating
the development of programs and applications using existing components
which can be separately designed and developed without knowledge of
each other.
As a first step in this direction Timor distinguishes type
definitions (introduced by the keyword type) from their implementations
(keyword
impl), cf. [3, 4]. An implementation, unlike a class in the conventional
OO paradigm, is not a type. A type can have multiple implementations
and these can re-use other implementations. There is an implicit
mechanism for using default implementations of types. The separation of types
and their implementations leads in turn to a separation of subtyping
and the re-use of existing implementations.
Subclassing (code inheritance) is abandoned in favour of a more general
technique which allows existing implementations of the same or of a
different type to be re-used in an implementation of a type. With the
help of this technique Timor can reverse the subclassing relationship,
such that a base type (e.g. Queue) can be implemented by re-using any
implementation of its derived type (e.g. DoubleEndedQueue), cf. [3].
One advantage of this approach is that the information hiding principle
[11] is naturally upheld. Timor's approach with respect to multiple
inheritance involving a common abstract ancestor is described in [4].
Since [4] and [3] were written some ideas have been refined and syntactic
improvements have been made to Timor. However, the essence of the papers
remains valid. The new syntax is explained and used in this paper.
Because
aggregation can be seen as an alternative to parts inheritance, section
2 describes aggregation in Timor, while section 3 discusses
the relationship between aggregation and inheritance. Section 4 then
turns to inheritance as such, briefly describing the structure of types
and their implementations. Because repeated inheritance is regarded
as fundamental, section 5 describes how it is handled in Timor at the
type level; the section concludes by describing how colliding methods
from unrelated ancestors can be handled. In section 6 implementation
inheritance is presented, using the revised Timor syntax. Section 7
discusses related work and section 8 concludes the paper. Diamond inheritance
is discussed in a companion paper [6].
2 AGGREGATION IN TIMOR TYPES
In Timor the types Radio and CassettePlayer
might be defined as follows:

To create a type whose instances combine the features of both types
aggregation
can be used, e.g.

Because Timor rigorously adheres to the information hiding principle,
this type definition uses abstract variables which can in
the present example be considered
as a shorthand notation for the following2:

A programmer of the new type does not have to provide an explicit
implementation of the methods corresponding to the aggregated items.
If the programmer simply
includes a corresponding concrete variable in his code, i.e.

the compiler automatically adds a standard
implementation, along the following lines:

If a type definition consists only of
aggregated variables (i.e. it corresponds to a struct or record), the
compiler produces a standard
implementation for
the entire type, e.g.:

However, the implementation programmer can, if he chooses,
take advantage of the information hiding principle by implementing
the set and get
methods in
some other way, and where appropriate can also use different data structures
[5].
Despite this mechanism a client programmer sees no noticeable difference
from the normal OO paradigm. He can for example invoke the methods
of the aggregated
radio part of an instance of this type and he can assign a new value to it,
e.g.

Such statements are interpreted by the compiler as method invocations,
e.g.

The compiler can of course make
optimisations, so that in the normal case there need be no loss of
efficiency compared with aggregation
in other languages.
It would violate the information hiding principle
to allow references to internal variables of an object to exist outside
the object (especially
as equivalent
concrete variables might not exist in a non-standard implementation of the
type), so a statement in the C++ style such as

is not supported.
3 AGGREGATION AND INHERITANCE
If individual types can be incorporated
into a new type by aggregation, why should one wish to use inheritance
as an alternative in such a
case? What inheritance
allows, but aggregation does not, are the following possibilities:
- If a subtype
object (e.g. a radio double cassette player) has been assigned
to a supertype reference (e.g. a radio), a client programmer can
use a
conditional
downcast statement on that reference to gain access to the entire object.
- With inheritance the implementor of a derived type can override
the methods associated with the part, but in the normal object oriented
paradigm he cannot
override the methods of an aggregated variable.
- With inheritance the methods
of a part become methods of the derived type as a whole. When aggregation
is used the aggregated variable also has to be
named in order to invoke a method.
On the other hand, if a part is inherited
- a new value cannot be assigned
to it as if it were an aggregated variable and its value cannot be copied
to another variable.
In other words, the two forms of modelling are by no means equivalent.
How significant are the differences?
Polymorphic Use of Inherited Parts
The advantages of inclusion polymorphism
[2] are well known and need not be repeated here. If the present example
were defined in terms
of inheritance,
it would be
possible to treat an object of type RadioDoubleCassettePlayer as
if it were simply of type Radio or of type CassettePlayer by
assigning a reference for the combined
object to a reference variable of the appropriate type.
Furthermore it would
be possible, using an appropriate downcast statement, to examine such a
reference with the aim of accessing the device as a whole.
Overriding
the Methods
In the example, the radio and the cassette player parts
each have methods for switching the device on and off. With aggregation,
the two parts remain
from
this point of view quite distinct components of the combined device, i.e.
there are two separate devices under one cover, which have separate switches.
With
a good inheritance scheme the designer of the new type gains the freedom
(without the compulsion) to merge such methods, so that for example a single
switch
turns both devices on/off. With the usual understanding of aggregation,
such freedom
does not exist (but see [10]).
Naming Parts
For the kind of example under discussion the fact that
modelling by aggregation requires the parts to be individually named
when a client
invokes methods
is not a serious hindrance. In fact the explicit naming of inherited parts
can
be an advantage, because it solves the naming problems which arise with
repeated inheritance and with clashing method names.
Assigning Values to
and from a Part
A new value can normally be assigned to an abstract
variable in Timor by invoking the associated "set" method.
However, this can be prevented by declaring the abstract variable as
final, which has the effect that there is a "get" method
but no "set" method. There is no way of preventing a "get" method
from being invoked. This is conceptually equivalent to the way public fields
are managed in Java, for example.
With inheritance the situation is different,
in that inheritance does not normally provide a way of accessing the
state of a supertype as if it were
a variable
with a separate value, and with good reason. To treat it in this way
would imply that a supertype has a separable state within its subtype.
But this
notion frequently
does not correspond to the kinds of situation in which inheritance is
used. For example if a concrete subtype List is derived from an abstract
supertype
Collection,
there is no notion that the List contains a Collection as a separate
part. In such a case the inheritance of methods, not of state, is in
the foreground.
But
even when the idea of inheriting state seems relevant (as in the inheritance
of two CassettePlayer parts in a RadioDoubleCassettePlayer), there is
no guarantee that these parts can be considered as totally separate.
Normally
the idea of
inheritance is that the supertype becomes an integral part of the subtype,
so that the copying of "values" of apparently separate parts
is a dangerous notion. This is underlined by the fact that methods can
be merged
and redefined
as part of the derived type's definition. What is the separate state
of a CassettePlayer part of a DoubleCassettePlayer, for example, if the
switch
methods of the individual
CassettePlayer parts are merged? Redefining methods implies changing
their behaviour and this can imply the merging of part states.
4 BASE
TYPES AND DERIVED TYPES
Having established that inheritance and aggregation
differ, we now turn to the issue of parts inheritance as such. Timor
recognises two
forms
of type
derivation,
distinguished by the keywords extends (which is intended to signal
the programmer's intention to define a behaviourally conform subtype
[8])
and includes (which
signals interface inheritance without subtyping). A type definition
can include both forms of derivation, in which case the resulting type
can
be used polymorphically
as a subtype of those types which it extends but not with respect to
those which it includes. Because the structure of extends and includes sections
are identical,
includes sections are not discussed explicitly. The general structure
of a derived type is:

An abstract type cannot be instantiated. It can have one or more implementations
and at the type level it can predefine makers for its subtypes; these
are distinguished by the keyword ThisType [4]. An implementation of
an abstract
type can also
implement makers for re-use, but these cannot be directly invoked by
clients.
The redefines and instance sections include only public methods.
There is a fundamental difference between method redefinition (which
can
appear in
redefines
sections of type definitions and refers to the redefinition of behaviour), and
code overriding (which is an implementation technique).
An implementation
of a type has the following basic structure:

The instance section includes both public and private methods. The
special overrides section described in earlier papers is no longer
needed. As
a result of a further
simplification there is no separate reuses section. We shall see later
how the idea of re-use can be expressed in the state section.
The order of the sections in a type definition and in an
implementation can vary and any section can appear more than once.
5
REPEATED TYPE INHERITANCE
In OO languages repeated inheritance is often
neglected or totally ignored. This is sometimes regarded as unimportant,
because in many
cases aggregation
can be
used as an alternative. However, that is not a satisfactory solution,
because, as discussed in section 3, aggregation and inheritance are
really quite
different in their effects. Hence repeated inheritance is treated as
fundamental in
the Timor design. In this respect it has an advantage over other object
oriented languages: by separating types and implementations, these
aspects of repeated
inheritance can be managed separately. In this section we consider
only type inheritance aspects of repeated parts inheritance.
Defining
Repeated Parts
The naming issue which arises with repeated inheritance
has influenced the structure of the clauses which appear in an extends section. Each
clause involving repeated
inheritance has the same basic form as the declaration of abstract
variables in the instance section of a type definition (and the declaration
of
concrete variables in a state section of an implementation). Thus a
type DoubleCassettePlayer defined using multiple inheritance has a similar appearance to aggregation,
i.e.

Syntactically it differs from aggregation only in that the parts appear
in an extends section rather than an instance section. Semantically
it differs
from
aggregation in the ways described in section 3. Because repeated inheritance
is involved, the identifiers cp1 and cp2 are essential for naming purposes.
But they also convey the notion that state and the associated methods
are replicated without implying that the part states are entirely separate
(in contrast with
aggregation). The degree of integration/separation is defined, from
the client's viewpoint, not in the extends section, but in the redefines section,
where,
as
we shall see shortly, methods can be merged.
When referring to one of the CassettePlayer parts, the client
programmer uses the dot notation, as he would for an aggregated abstract
variable,
e.g.

Syntactically, the use of the dot notation here
deliberately resembles that in the standard object oriented paradigm,
but semantically cp1.switchOn is
not an
object with its method, but is rather the non-decomposable name of
a method of dcp as such. Thus (in accordance with the information hiding
principle
[11]) the use of the dot notation does not imply that there is actually
a variable
cp1 in all implementations of the type (although there may be in some
implementations). Rigorously applying the information hiding principle
implies that the implementor
of a type can implement the type in any way that he sees fit, provided
that his
implementation conforms with the specification. Formally the instance
methods of DoubleCassettePlayer which need to be implemented are as
follows:
instance:

and there is a default parameterless maker init().
Merging Individual Methods As defined above, an instance of the type
DoubleCassettePlayer can almost be regarded as two separate units which
have been put into a
single box.
These
parts can, for example, be switched on and off separately. However,
a more integrated
unit might be modelled such that its instances are activated by a single
switching mechanism. At the type level this involves redefining the
switching methods
in such a way that the client sees only one such mechanism. But then
he might need
to have some further methods for determining the mode in which the
device should be active. To do this he could define a type as follows:

The switching methods are here redefined in such a way that there
is only one switching mechanism for the combined device. The syntax
[cp1,
cp2] lists the
parts affected by the redefinition of a method. The corresponding method
then no longer exists as separate methods for the listed parts, but
is subsumed into a single method which does not have a "part" name,
i.e. the device as a whole is switched on using a statement such as:

Hence the instance methods of the type IntegratedDoubleCassettePlayer are as follows:

and there is a default parameterless maker init().
Merging all the Methods of a View
Although the above syntax is relatively
short, it does involve separately listing each method to be merged.
Timor provides an interface definition
mechanism,
known as a view, which allows related sets of methods to be grouped
together, e.g.

Views are groupings of instance methods which can usefully be incorporated
into different types. They may be defined retrospectively, such that
if all the methods
defined in a view occur as instance methods of a pre-existing type
a match occurs3. Thus the view Switchable matches the types Radio and
CassettePlayer.
In order
to shorten redefinitions of methods and make them more easily intelligible,
a redefines clause can rename all the methods of a view together. Thus
the type
IntegratedDoubleCassettePlayer could have been defined as follows:

Semantically this is equivalent to the method by method definition.
Polymorphic
Use of Repeated Parts
If an object defined via repeated inheritance
is assigned to a supertype variable which has the type of a repeated
part, it is necessary to
make clear which
part is intended, i.e. which methods of the object are to be invoked
polymorphically. To achieve this, the dot notation is used, as follows:

If a method associated
with the part has been redefined, the method into which it has been
merged is invoked polymorphically (using the
dynamic
scheduling mechanism) as if it were the original method of the supertype,
as the following
code illustrates.

Invoking merged methods
polymorphically may not be appropriate, but the programmer can easily
avoid this in relevant cases by defining
parts
in an includes
clause rather than an extends clause.
The Cast Statement
Semantically objects, not their parts, are assigned
polymorphically to variables of supertypes, as in other OO languages,
so that although
it
appears that
only a part has been assigned, the entire object dcp is actually assigned
to the
variable cp. Hence a conditional downcast can be used to gain access
to other parts of
the object, e.g.

This is the normal
Timor cast statement. In the example the clause IntegratedDoubleCassettePlayer
idcp would be selected, and the entire
object would be accessible using
the name idcp. However, in this form the programmer would not (easily)
be able
to determine
which part had been assigned to cp. If this is important more specific
clauses can be used in the cast statement, e.g.

In the example the
second of these clauses would be selected, because the part used to
assign the object to cp was cp2.
Casting takes repeated inheritance
into account by supporting repetition, e.g.

A clause with square brackets indicates that the associated statements
are repeated, i.e. if the object to which cp refers contains one or
more CassettePlayer parts,
they are executed for each matching part. (The Radio
r statements are
not repeated.)
In such cases it can be useful to execute not simply the
code block for the first matching clause, but for example all the clauses
which
match.
Hence
the keyword
as following the reference can be followed by a further keyword: firstof,
anyof or allof, with the obvious meanings, whereby anyof non-deterministically
selects
any matching type. The default is firstof.
Non-repeated Parts
The idea that a base type can be given a part name
is essential for repeated parts, but it can also be essential for identifying
which
methods of
base types are to be merged, even for cases not involving repeated
inheritance. Furthermore,
the use of part names for non-repeated parts can sometimes add symmetry.
For these reasons Timor allows part names to be provided for base type
parts not
involving repeated inheritance. To illustrate this, consider an IntegratedRadioDoubleCassettePlayer device:

which inherits from a type Radio as defined in section 2. Allowing
a part name to be used in such a case not only allows appropriate methods
to be
redefined.
It also allows clients to access the different parts uniformly.
Sometimes
separate methods with identical signatures might be derived from different
base types not involving repeated inheritance. This
happens, for example, in
a simple type RadioCassettePlayer defined without using part names,
e.g.

because each base type has the Switchable methods, and the intention
is to keep them separate. In this case they are not automatically merged
as
described
in
[4], because a view is not regarded as a common ancestor. A compile
time error occurs if the names are not resolved in the type definition.
One way of keeping the methods separate is to give the two
base types part names, but then the client is forced always to use
part names.
To give
greater freedom
in such situations Timor allows optional part names to be defined,
as follows:

The client of such a type has the freedom to use part names or not,
as it suits him, except in the case where this creates ambiguities.
Thus
he could
write:

If a client assigns such an
object to a view reference, this is potentially ambiguous, so he must
use the appropriate part name, e.g.
 From the implementor's viewpoint method names
have no part name except where this would be ambiguous.
This technique
can be used to handle entirely coincidental method collisions, such
as arise in a type GraphicalCardDealer (cf. [1]), in which each
of the base types CardDealer and GraphicalComponent have a parameterless
method
draw, with
quite different semantics:

Here part names disambiguate the colliding methods. The result is
two separate draw methods, which from the client's viewpoint can easily
be distinguished
as dealer.draw() and graphics.draw().
It would suffice for only one of the base
types to be given a part name: the names of methods of the other would
then simply be used without a part name.
6 IMPLEMENTING THE TYPES
Some key issues which must be understood
about implementations in Timor are (a) that they are not themselves
types, and (b) that each implementation
must be
a complete implementation of its type. However, a complete implementation
does not necessarily mean a totally fresh implementation. At an earlier
stage
in
the development of Timor a code re-use technique was proposed [3, 4]
which permitted
extensive re-use of existing code in a much more flexible manner than
subclassing allows.
Following the crystallisation of ideas with respect to repeated
inheritance the details of that re-use technique have been revised
and simplified,
without changing
the basic concepts described in earlier papers. We begin with a simple
example which was presented in [3] to illustrate how subclassing can
be simulated.
The extra notion now associated with the re-use technique, initially
introduced to
accommodate repeated inheritance, is that re-used code has a state,
and so can be regarded as a variable in the state section. The effect
of
this change
is
that the mechanism has an interesting relationship with some forms
of delegation, and we therefore motivate the discussion in this direction.
Relationship
to Delegation
The standard OO code inheritance technique incrementally
extends the code of a class in its subclass(es). This can be a problem
if subclassing
and
subtyping
do not match each other. One way of avoiding an undesirable type relationship
while nevertheless re-using code in the standard OO paradigm would
be to use delegation. In the following we see how Timor can improve
on this
approach.
In Timor a type DoubleEndedQueue might be derived from a type
Queue by inclusion (not by extension, as that would imply a behavioural
subtype
relationship).
Given an implementation of DoubleEndedQueue, this implementation
could be re-used to
implement Queue by declaring a DoubleEndedQueue as an internal variable
(cf. delegation). We begin with the type definitions. 
Let us assume that an implementation exists for DoubleEndedQueue which
we want to re-use to implement Queue. This could be achieved in Timor
as follows:

This has the nice property that any implementation of DoubleEndedQueue
can be re-used, according to the normal rules of Timor. However, using
delegation
in
this way is not only inefficient at run-time; it is also wasteful of
programmer effort, because methods which in effect already exist in
the implementation
of DoubleEndedQueue have to be invoked indirectly via QueueImpl. Both
these disadvantages
can be eliminated, by providing a mechanism which allows the programmer
to define that those methods associated with the variable deq which
match the
methods of
Queue should in fact be treated directly as methods of Queue. This
is in effect the mechanism which was defined in [3, 4], except that
the
re-used
code was
not defined as a variable but as a type. The matching rules are as
defined in those
papers.
Syntactically re-use variables are distinguished from other
variables in the state section by prefixing them with a hat (^) symbol.
As previously,
more
than one item can be re-used, and matching occurs in the order of the
declarations.
Here is how the example would actually appear in Timor:

In this case all the public methods of Queue can be matched in DoubleEndedQueue.
A match is not sought if an interface method of the type being implemented
is explicitly coded in the instance section of an implementation.
In
the present example it would in principle be possible to re-use the
maker defined in DoubleEndedQueue, but allowing makers to be matched
leads to problems
in the general case, e.g. when more than one re-use variable is involved.
Consequently only instance methods can be matched.
Imitating Subclassing
The above example does not illustrate how subclassing
can be imitated in cases where the subclass requires access to the
internal variables
and
methods of
the superclass. To achieve this we follow a variant of the same principle,
this time
allowing a declaration of a re-use variable to be defined in terms
of a specific implementation rather than a type. This technique was
also
described
in an
earlier form in [3].
To illustrate this approach, we assume that an
implementation ArrayQueue1 of Queue exists (cf. [3]). The aim is to
extend this incrementally
to provide an implementation of DoubleEndedQueue (cf. ArrayDEQ1 [3])
in
the subclassing
style,
as follows:

As was envisaged for the original re-use technique, nominating an
implementation for re-use gives access to the internal methods and
state of that implementation.
Because in the revised approach it appears as a re-use variable, no
special "super" keyword
or special syntax is necessary for accessing or overriding these. (Hence
the earlier overrides section has also become redundant.)
To make the
code less tedious to write and easier to understand, Timor also supports
a Pascal-like with statement, e.g.

This allows the programmer to use internal names exactly as they
appear in the implementation being re-used.
Implementing Repeated
Inheritance
An important advantage of the revised Timor re-use technique
is that it takes into account not only code re-use but
also state re-use. This
greatly
simplifies
the implementation of types which use repeated inheritance. We begin
with a type RadioDoubleCassettePlayer:

which can be implemented as follows:

If a part name appears in a type definition, this is significant
for interface method matching purposes. If an implementation variable
uses
an identifier
which differs from a part name in the type definition, a match does
not occur. But
as the Queue example illustrates, a base type in a type definition
which is unnamed can be matched with a re-use variable regardless of
its identifier,
as all variables
in an implementation must have identifiers.
This example illustrates that a straightforward
mapping can exist from each named part in a type definition to
an implementation
of that part,
provided
that it
has a parameterless maker init(). If, as in the above example, the
type consists entirely of named parts (and/or abstract variables) the
compiler
automatically
produces a standard implementation of the entire type (named after
the type with the standard suffix Impl).
Each implementation must be
complete in itself. Consequently a derived type need not re-use
existing implementations (and on the other hand
a base type
can re-use
implementations of other types). Thus an implementor of RadioDoubleCassettePlayer
can provide a completely fresh implementation. In this case all the
public methods of the type have to be completely implemented. The new
implementation
must name
all the methods unambiguously, e.g.

Using Redefined Methods
Even when public methods of a re-use variable
have been redefined in a re-using type, these still exist internally
as invocable methods.
This is illustrated
by a partial implementation of IntegratedRadioDoubleCassettePlayer,
showing how its redefined switching methods might be implemented:

7 RELATED WORK
Eiffel [9] supports repeated inheritance primarily
by allowing individual features to be renamed. There is no explicit
support
for the idea of "parts" which
can be given separate identifiers. Consequently the replication of
a part involves renaming each feature individually. Furthermore
the introduction of arbitrary
new names for features creates problems (which can be resolved in select
clauses) that do not arise in Timor, where arbitrary renaming is
avoided in favour of
the qualification of existing names by means of part identifiers. Eiffel's
use of repeated inheritance to imitate a "super" construct
by renaming methods in the "super instance" and redefining
and selecting them in the "subtype instance" is of course
unnecessary in Timor.
Sather [13] distinguishes between subtyping and
subclassing. At the subtyping level there is no renaming facility,
so that methods
with
identical (or
in some cases with contravariantly conform) signatures inherited from
different supertypes
are automatically merged. Although it appears to be possible repeatedly
to
inherit from the same supertype, this leads to the effect that the
repeatedly inherited
methods are merged. At the level of code re-use the code of multiple
concrete classes can be included into another class and here renaming
is possible.
Repeated code inclusion leads to replication, and in this case clashing
features must
be individually renamed.
Theta [7], like Timor strictly separates types
from their implementations. At the type level multiple inheritance
is possible, and methods can
be individually renamed. There is no concept equivalent to a part identifier.
At the implementation
level only one base class can be re-used, independently of type relationships.
A parts concept does not occur at the type or implementation level.
Although
C++ [14] supports multiple inheritance and repeated inheritance
the latter can only occur indirectly, i.e. in conjunction with diamond
inheritance. As the latter is the theme of a companion paper [6], a
fuller discussion
of
C++ diamond inheritance, including the nomination of base classes as
virtual, is
discussed there. However, it is of particular interest to the present
paper that a client must resolve the names of conflicting methods by
qualifying
them with
the names of the classes in which they are defined. In contrast, by
allowing the definer of a type to qualify conflicting methods with
freely chosen
part identifiers Timor can handle repeated inheritance in an unproblematic
way.
Java [1] supports multiple inheritance only at the type level
(via interfaces) and provides no explicit support for repeated
inheritance.
Because multiply
inherited methods with identical signatures are automatically merged,
even simple arbitrary
collisions create a problem.
Timor's separation of types and implementations
allows these aspects of inheritance to be handled orthogonally.
As was indicated in section
6 re-use
variables
can be seen as an efficient mechanism for some cases of delegation,
and they can
be used either to imitate subclassing or to reverse subclassing. The
latter has the advantage that it is easier to conform with the information
hiding
principle,
with the consequence that in Timor any implementation of an appropriate
type can be "re-used" to implement another, independently
of type relationships. This approach derives from the re-use technique
proposed by Schmolitzky in [12],
but differs from his proposal by adding the idea of state to the re-use
technique, thus opening the way for a straightforward implementation
of the kind of repeated
inheritance discussed in this paper, and simplifying the constructs
needed, e.g. by making special support for "super" superfluous
(which is especially helpful in the context of multiple and repeated
implementation
inheritance).
Re-use variables superficially resemble the delegation
technique used in prototype based languages. In each case "missing" interface
methods are supplied from variables declared within the implementation.
However, the differences
are overwhelming. In Timor this is merely an implementation technique,
not affecting
type relationships; re-use variables are implemented by value, not
by pointer; the search for methods occurs at compile-time, not at run-time;
the search
is not recursive, etc.
8 CONCLUDING REMARKS
This paper has presented the
Timor approach to parts inheritance and repeated parts inheritance.
This is characterised by the idea
that
individual inherited
parts of a subtype can be provided with part identifiers. This technique,
which to our knowledge does not appear in other OO languages, has at
least the following
advantages:
- Members of repeated parts can be unambiguously qualified
using a part name.
- Individual members of parts with part names
can easily be combined (optionally) into a single member of
the subtype.
- At the implementation level the part names can appear
as re-use variable identifiers, thus simplifying the implementation
of types
defined in
terms of repeated inheritance.
This also eliminates the need for a "super" construct and
the naming problems to which this gives rise for multiple code inheritance.
In
[4] we argued that different kinds of multiple inheritance are best
handled by different mechanisms. In that paper we then concentrated
on multiple inheritance
from a common abstract ancestor, which has more in common with conventional
type inheritance in OO languages. The relationship between that approach
and the parts
approach described in this paper, and in particular the issues which
appear when they are combined, arise typically in cases of diamond
inheritance from a common
concrete ancestor. For example a base type Person might be specialised
in different orthogonal ways (e.g. as a Student and as an Employee),
and
these
can then
be brought together to create a type EmployedStudent. This can also
involve repeated
inheritance (e.g. a DoublyEmployedStudent). In such cases some of
the elements of both forms of multiple type inheritance discussed
earlier
occur in combination.
These issues are discussed in a companion paper [6].
ACKNOWLEDGEMENTS
Special thanks are due to Dr. Mark Evered and
Dr. Axel Schmolitzky for their invaluable contributions to the
ideas which have been
taken over
from earlier
projects. Without their ideas and comments Timor would not have
been possible.
Footnotes
1 see http://www.timor-programming.org
2 Abstract
variables are discussed in greater detail in [5], where the implications
of nested abstract variables are examined.
3 Although a view can basically
be considered as equivalent to a type, it is not regarded as a
type
with respect to the rules for handling common ancestors
in
multiple inheritance, see [6].
REFERENCES
[1] K. Arnold, J. Gosling, and D. Holmes, The Java Programming
Language, Third Edition. Addison-Wesley, 2000.
[2] L. Cardelli and P. Wegner, "On Understanding
Types, Data Abstraction and Polymorphism," Computing Surveys,
vol. 17, no. 4, pp. 471-522, 1985.
[3] J. L. Keedy, G. Menger, and
C. Heinlein, "Support for Subtyping
and Code Re-use in Timor," 40th International Conference on
Technology of Object-Oriented Languages and Systems (TOOLS Pacific 2002), Sydney,
Australia, 2002, Conferences in Research and Practice in Information
Technology, vol. 10, pp. 35-43.
[4] J. L. Keedy, G. Menger, and C.
Heinlein, "Inheriting from
a Common Abstract Ancestor in Timor," Journal of Object Technology,
vol. 1, no. 1, May 2002, pp. 81-106. http://www.jot.fm/issues/issue_2002_05/article2.
[5]
J. L. Keedy, G. Menger, and C. Heinlein, "Taking Information
Hiding Seriously in an Object Oriented Context," Net.ObjectDays,
Erfurt, Germany, 2003, pp. 51-65.
[6] J. L. Keedy, G. Menger, and C.
Heinlein, "Diamond Inheritance
and Attribute Types in Timor", in Journal of Object Technology,
vol. 3, no. 10, November-December 2004, pp. 121-142. http://www.jot.fm/issues/issue_2004_11/article2
[7]
B. Liskov, D. Curtis, M. Day, S. Ghemawat, R. Gruber, P. Johnson, and
A. C. Myers, "Theta Reference Manual," MIT Laboratory
for Computer Science, Cambridge, MA, Programming Methodology Group
Memo 88, February 1994.
[8] B. Liskov and J. M. Wing, "A Behavioral
Notion of Subtyping," ACM
Transactions on Programming Languages and Systems, vol. 16, no. 6,
pp. 1811-1841, 1994.
[9] B. Meyer, Eiffel: the Language. New York.
Prentice-Hall, 1992.
[10] M. Mezini, "Dynamic Object Evolution
without Name Collisions," ECOOP
'97, 1997, Springer Verlag, LNCS, vol. 1241, pp. 190-219.
[11] D. L.
Parnas, "On the Criteria To Be Used in Decomposing
Systems into Modules," Communications of the ACM, vol. 15, no.
12, pp. 1053-1058, 1972.
[12] A. Schmolitzky, "Ein Modell zur Trennung
von Vererbung und Typabstraktion in objektorientierten Sprachen (A
Model for Separating
Inheritance and Type Abstraction in Object Oriented Languages)," Ph.D.
Thesis, Dept. of Computer Structures: University of Ulm, Germany, 1999.
[13]
D. Stoutamire and S. Omohundro, "The Sather 1.1 Specification," International
Computer Science Institute, Berkley, CA ICSI Technical Report TR-96-012,
1996.
[14] B. Stroustrup, The C++ Programming Language, Third Edition. Addison Wesley, 1997.
About the author
Cite this article as follows: Leslie Keedy, Christian Heinlein, Gisela
Menger: "Inheriting Multiple and Repeated Parts in Timor",
in Journal of Object Technology, vol. 3, no. 10, November-December
2004, pp. 99-120. http://www.jot.fm/issues/issue_2004_11/article1
|