Rationale for Ada 2005
2.4 Interfaces
In Ada 95, a derived type can really only have one
immediate ancestor. This means that true multiple inheritance is not
possible although curious techniques involving discriminants and generics
can be used in some circumstances.
General multiple inheritance
has problems. Suppose that we have a type T
with some components and operations. Perhaps
type T is tagged
record
A: Integer;
B: Boolean;
end record;
procedure Op1(X: T);
procedure Op2(X: T);
Now suppose we derive
two new types from T thus
type T1 is new T with
record
C: Character;
end record;
procedure Op3(X: T1);
-- Op1 and Op2 inherited, Op3 added
type T2 is new T with
record
C: Colour;
end record;
procedure Op1(X: T2);
procedure Op4(X: T2);
-- Op1 overridden, Op2 inherited, Op4 added
Now suppose that we
were able to derive a further type from both T1
and T2 by perhaps writing
type TT is new T1 and T2 with null record; -- illegal
This is about the simplest example one could imagine.
We have added no further components or operations. But what would TT
have inherited from its two parents?
There is a general rule that a record cannot have
two components with the same identifier so presumably it has just one
component A and one component B.
But what about C? Does it inherit the character
or the colour? Or is it illegal because of the clash? Suppose T2
had a component D instead of C.
Would that be OK? Would TT then have four
components?
And then consider the operations. Presumably it has
both Op1 and Op2.
But which implementation of Op1? Is it the
original Op1 inherited from T
via T1 or the overridden version inherited
from T2? Clearly it cannot have both. But
there is no reason why it cannot have both Op3
and Op4, one inherited from each parent.
The problems arise when inheriting components from
more than one parent and inheriting different implementations of
the same operation from more than one parent. There is no problem with
inheriting the same specification of an operation from two parents.
These observations provide the essence of the solution.
At most one parent can have components and at most one parent can have
concrete operations – for simplicity we make them the same parent.
But abstract operations can be inherited from several parents. This can
be phrased as saying that this kind of multiple inheritance is about
merging contracts to be satisfied rather than merging algorithms or state.
So Ada 2005 introduces the concept of an interface
which is a tagged type with no components and no concrete operations.
The idea of a null procedure as an operation of a tagged type is also
introduced; this has no body but behaves as if it has a null body. Interfaces
are only permitted to have abstract subprograms and null procedures as
operations.
We will outline the ways in which interfaces can
be declared and composed in a symbolic way and then conclude with a more
practical example.
We might declare a
package Pi1 containing an interface Int1
thus
package Pi1 is
type Int1 is interface;
procedure Op1(X: Int1) is abstract;
procedure N1(X: Int1) is null;
end Pi1;
Note the syntax. It uses the new reserved word interface.
It does not say tagged although all interface types are tagged.
The abstract procedure Op1 has to be explicitly
stated to be abstract as usual. The null procedure N1
uses new syntax as well. Remember that a null procedure behaves as if
its body comprises a single null statement; but it doesn't actually have
a concrete body.
The main type derivation
rule then becomes that a tagged type can be derived from zero or one
conventional tagged types plus zero or more interface types. Thus
type NT is new T and Int1 and Int2 with
... ;
where
Int1 and
Int2
are interface types. The normal tagged type if any has to be given first
in the declaration. The first type is known as the parent so the parent
could be a normal tagged type or an interface. The other types are known
as progenitors. Additional components and operations are allowed in the
usual way.
The term progenitors may seem strange but the term
ancestors in this context was confusing and so a new term was necessary.
Progenitors comes from the Latin progignere, to beget, and so is very
appropriate.
It might have been thought that it would be quite
feasible to avoid the formal introduction of the concept of an interface
by simply saying that multiple parents are allowed provided only the
first has components and concrete operations. However, there would have
been implementation complexities with the risk of violating privacy and
distributed overheads. Moreover, it would have caused maintenance problems
since simply adding a component to a type or making one of its abstract
operations concrete would cause errors elsewhere in the system if it
was being used as a secondary parent. It is thus much better to treat
interfaces as a fundamentally new concept. Another advantage is that
this provides a new class of generic parameter rather neatly without
complex rules for instantiations.
If the normal tagged
type T is in a package Pt
with operations Opt1, Opt2
and so on we could now write
with Pi1, Pt;
package PNT is
type NT is new Pt.T and Pi1.Int1 with ... ;
procedure Op1(X: NT); -- concrete procedure
-- possibly other ops of NT
end PNT;
We must of course provide a concrete procedure for
Op1 inherited from the interface Int1
since we have declared NT as a concrete type.
We could also provide an overriding for N1
but if we do not then we simply inherit the null procedure of Int1.
We could also override the inherited operations Opt1
and Opt2 from T
in the usual way.
Interfaces can be composed
from other interfaces thus
type Int2 is interface;
...
type Int3 is interface and Int1;
...
type Int4 is interface and Int1 and Int2;
...
Note the syntax. A tagged type declaration always
has just one of interface, tagged and with (it doesn't
have any if it is not a tagged type). When we derive interfaces in this
way we can add new operations so that the new interface such as Int4
will have all the operations of both Int1
and Int2 plus possibly some others declared
specifically as operations of Int4. All these
operations must be abstract or null and there are fairly obvious rules
regarding what happens if two or more of the ancestor interfaces have
the same operation. Thus a null procedure overrides an abstract one but
otherwise repeated operations must have profiles that are type conformant
and have the same convention.
We refer to all the interfaces in an interface list
as progenitors. So Int1 and Int2
are progenitors of Int4. The first one is
not a parent – that term is only used when deriving a type as opposed
to composing an interface.
Note that the term ancestor covers all generations
whereas parent and progenitors are first generation only.
Similar rules apply
when a tagged type is derived from another type plus one or more interfaces
as in the case of the type NT which was
type NT is new T and Int1 and Int2 with ... ;
In this case it might be that T
already has some of the operations of Int1
and/or Int2. If so then the operations of
T must match those of Int1
or Int2 (be type conformant etc).
We informally speak of a specific tagged type as
implementing an interface from which it is derived (directly or indirectly).
The phrase "implementing an interface" is not used formally
in the definition of Ada 2005 but it is useful for purposes of discussion.
Thus in the above example the tagged type NT
must implement all the operations of the interfaces Int1
and Int2. If the type T
already implements some of the operations then the type NT
will automatically implement them because it will inherit the implementations
from T. It could of course override such inherited
operations in the usual way.
The normal "going
abstract" rules apply in the case of functions. Thus if one operation
is a function F thus
package Pi2 is
type Int2 is interface;
function F(Y: Int2) return Int2 is abstract;
end Pi2;
and T
already has such a conforming operation
package PT is
type T is tagged record ...
function F(X: T) return T;
end PT;
then in this case the type
NT
must provide a concrete function
F. See however
the discussion in Section
2.7) for the case
when the type
NT has a null extension.
Class wide types also
apply to interface types. The class wide type Int1'Class
covers all the types derived from the interface Int1
(both other interfaces as well as normal tagged types). We can then dispatch
using an object of a concrete tagged type in that class in the usual
way since we know that any abstract operation of Int1
will have been overridden. So we might have
type Int1_Ref is access all Int1'Class;
NT_Var: aliased NT;
Ref: Int1_Ref := NT_Var'Access;
Observe that conversion is permitted between the
access to class wide type Int1_Ref and any
access type that designates a type derived from the interface type Int1.
Interfaces can also be used in private extensions
and as generic parameters.
Thus
type PT is new T and Int2 and Int3 with private;
...
private
type PT is new T and Int2 and Int3 with null record;
An important rule regarding private extensions is
that the full view and the partial view must agree with respect to the
set of interfaces they implement. Thus although the parent in the full
view need not be T but can be any type derived
from T, the same is not true of the interfaces
which must be such that they both implement the same set exactly. This
rule is important in order to prevent a client type from overriding private
operations of the parent if the client implements an interface added
in the private part.
Generic parameters
take the form
generic
type FI is interface and Int1 and Int2;
package ...
and then the actual parameter must be an interface
which implements all the ancestors Int1, Int2
etc. The formal could also just be type FI
is interface; in which case
the actual parameter can be any interface. There might be subprograms
passed as further parameters which would require that the actual has
certain operations. The interfaces Int1 and
Int2 might themselves be formal parameters
occurring earlier in the parameter list.
Interfaces (and formal
interfaces) can also be limited thus
type LI is limited interface;
We can compose mixtures of limited and nonlimited
interfaces but if any one of them is nonlimited then the resulting interface
must not be specified as limited. This is because it must implement the
equality and assignment operations implied by the nonlimited interface.
Similar rules apply to types which implement one or more interfaces.
We will come back to this topic in a moment.
There are other forms of interfaces, namely synchronized
interfaces, task interfaces, and protected interfaces. These bring support
for polymorphic, class wide object oriented programming to the real time
programming arena. They are described in Section
5.3.
Having described the general ideas in somewhat symbolic
terms, we will now discuss a more concrete example.
Before doing so it is important to emphasize that
interfaces cannot have components and therefore if we are to perform
multiple inheritance then we should think in terms of abstract operations
to read and write components rather than the components themselves. This
is standard OO thinking anyway because it preserves abstraction by hiding
implementation details.
Thus rather than having a component such as Comp
it is better to have a pair of operations. The function to read the component
can simply be called Comp. A procedure to
update the component might be Set_Comp. We
will generally use this convention although it is not always appropriate
to treat the components as unrelated entities.
Suppose now that we
want to print images of the geometrical objects. We will assume that
the root type is declared as
package Geometry is
type Object is abstract tagged private;
procedure Move(O: in out Object'Class; X, Y: in Float);
...
private
type Object is abstract tagged
record
X_Coord: Float := 0.0;
Y_Coord: Float := 0.0;
end record;
...
end;
The type Object is private
and by default both coordinates have the value of zero. The procedure
Move, which is class wide, enables any object
to be moved to the location specified by the parameters.
Suppose also that we
have a line drawing package with the following specification
package Line_Draw is
type Printable is interface;
type Colour is ... ;
type Points is ... ;
procedure Set_Hue(P: in out Printable; C: in Colour) is abstract;
function Hue(P: Printable) return Colour is abstract;
procedure Set_Width(P: in out Printable; W: in Points) is abstract;
function Width(P: Printable) return Points is abstract;
type Line is ... ;
type Line_Set is ... ;
function To_Lines(P: Printable) return Line_Set is abstract;
procedure Print(P: in Printable'Class);
private
procedure Draw_It(L: Line; C: Colour; W: Points);
end Line_Draw;
The idea of this package is that it enables the drawing
of an image as a set of lines. The attributes of the image are the hue
and the width of the lines and there are pairs of subprograms to set
and read these properties of any object of the interface Printable
and its descendants. These operations are of course abstract.
In order to prepare an object in a form that can
be printed it has to be converted to a set of lines. The function To_Lines
converts an object of the type Printable into
a set of lines; again it is abstract. The details of various types such
as Line and Line_Set
are not shown.
Finally the package Line_Draw
declares a concrete procedure Print which
takes an object of type Printable'Class and
does the actual drawing using the slave procedure Draw_It
declared in the private part. Note that Print
is class wide and is concrete. This is an important point. Although all
primitive operations of an interface must be abstract this does not apply
to class wide operations since these are not primitive.
The body of the procedure
Print could take the form
procedure Print(P: in Printable'Class) is
L: Line_Set := To_Lines(P);
A_Line: Line;
begin
loop
-- iterate over the Line_Set and extract each line
A_Line := ...
Draw_It(A_Line, Hue(P), Width(P));
end loop;
end Print;
but this is all hidden from the user. Note that the
procedure Draw_It is declared in the private
part since it need not be visible to the user.
One reason why the user has to provide To_Lines
is that only the user knows about the details of how best to represent
the object. For example the poor circle will have to be represented crudely
as a polygon of many sides, perhaps a hectogon of 100 sides.
We can now take at
least two different approaches. We can for example write
with Geometry, Line_Draw;
package Printable_Geometry is
type Printable_Object is
abstract new Geometry.Object and Line_Draw.Printable with private;
procedure Set_Hue(P: in out Printable_Object; C: in Colour);
function Hue(P: Printable_Object) return Colour;
procedure Set_Width(P: in out Printable_Object; W: in Points);
function Width(P: Printable_Object) return Points;
function To_Lines(P: Printable_Object) return Line_Set is abstract;
private
...
end Printable_Geometry;
The type Printable_Object
is a descendant of both Object and Printable
and all concrete types descended from Printable_Object
will therefore have all the operations of both Object
and Printable. Note carefully that we have
to put Object first in the declaration of
Printable_Object and that the following would
be illegal
type Printable_Object is
abstract new Line_Draw.Printable and Geometry.Object with private; -- illegal
This is because of the rule that only the first type
in the list can be a normal tagged type; any others must be interfaces.
Remember that the first type is always known as the parent type and so
the parent type in this case is Object.
The type Printable_Object
is declared as abstract because we do not want to implement To_Lines
at this stage. Nevertheless we can provide concrete subprograms for all
the other operations of the interface Printable.
We have given the type a private extension and so in the private part
of its containing package we might have
private
type Printable_Object is abstract new Geometry.Object and Line_Draw.Printable with
record
Hue: Colour := Black;
Width: Points := 1;
end record;
end Printable_Geometry;
Just for way of illustration,
the components have been given default values. In the package body the
operations such as the function Hue are simply
function Hue(P: Printable_Object) return Colour is
begin
return P.Hue;
end;
Luckily the visibility rules are such that this does
not do an infinite recursion!
Note that the information containing the style components
is in the record structure following the geometrical properties. This
is a simple linear structure since interfaces cannot add components.
However, since the type Printable_Object has
all the operations of both an Object and a
Printable, this adds a small amount of complexity
to the arrangement of dispatch tables. But this detail is hidden from
the user.
The key point is that
we can now pass any object of the type Printable_Object
or its descendants to the procedure
procedure Print(P: in Printable'Class);
and then (as outlined above) within Print
we can find the colour to be used by calling the function Hue
and the line width to use by calling the function Width
and we can convert the object into a set of lines by calling the
function To_Lines.
And now we can declare the various types Circle,
Triangle, Square
and so on by making them descendants of the type Printable_Object
and in each case we have to implement the function To_Lines.
The unfortunate aspect of this approach is that we
have to move the geometry hierarchy. For example the triangle package
might now be
package Printable_Geometry.Triangles is
type Printable_Triangle is new Printable_Object with
record
A, B, C: Float;
end record;
... -- functions Area, To_Lines etc
end;
We can now declare
a Printable_Triangle thus
A_Triangle: Printable_Triangle := (Printable_Object with A => 4.0, B => 4.0, C => 4.0);
This declares an equilateral triangle with sides
of length 4.0. Its private Hue and Width
components are set by default. Its coordinates which are also private
are by default set to zero so that it is located at the origin. (The
reader can improve the example by making the components A, B and C private as well.)
We can conveniently
move it to wherever we want by using the procedure Move
which being class wide applies to all types derived from Object.
So we can write
A_Triangle.Move(1.0, 2.0);
And now we can make
a red sign
Sign: Printable_Triangle := A_Triangle;
Having declared the
object Sign, we can give it width and hue
and print it
Sign.Set_Hue(Red);
Sign.Set_Width(3);
Sign.Print; -- print thick red triangle
As we observed earlier this approach has the disadvantage
that we had to move the geometry hierarchy. A different approach which
avoids this is to declare printable objects of just the kinds we want
as and when we want them.
So assume now that
we have the package Line_Draw as before and
the original package Geometry and its child
packages. Suppose we want to make printable triangles and circles. We
could write
with Geometry, Line_Draw; use Geometry;
package Printable_Objects is
type Printable_Triangle is new Triangles.Triangle and Line_Draw.Printable with private;
type Printable_Circle is new Circles.Circle and Line_Draw.Printable with private;
procedure Set_Hue(P: in out Printable_Triangle; C: in Colour);
function Hue(P: Printable_Triangle return Colour;
procedure Set_Width(P: in out Printable_Triangle; W: in Points);
function Width(P: Printable_Triangle) return Points;
function To_Lines(T: Printable_Triangle) return Line_Set;
procedure Set_Hue(P: in out Printable_Circle; C: in Colour);
function Hue(P: Printable_Circle) return Colour;
procedure Set_Width(P: in out Printable_Circle; W: in Points);
function Width(P: Printable_Circle) return Points;
function To_Lines(C: Printable_Circle) return Line_Set;
private
type Printable_Triangle is new Triangles.Triangle and Line_Draw.Printable with
record
Hue: Colour := Black;
Width: Points := 1;
end record;
type Printable_Circle is new Circles.Circle and Line_Draw.Printable with
record
Hue: Colour := Black;
Width: Points := 1;
end record;
end Printable_Objects;
and the body of the package will provide the various
subprogram bodies.
Now suppose we already
have a normal triangle thus
A_Triangle: Geometry.Triangles.Triangle := ... ;
In order to print A_Triangle
we first have to declare a printable triangle thus
Sign: Printable_Triangle;
and now we can set
the triangle components of it using a view conversion thus
Triangle(Sign) := A_Triangle;
And then as before
we write
Sign.Set_Hue(Red);
Sign.Set_Width(3);
Sign.Print_It; -- print thick red triangle
This second approach
is probably better since it does not require changing the geometry hierarchy.
The downside is that we have to declare the boring hue and width subprograms
repeatedly. We can make this much easier by declaring a generic package
thus
with Line_Draw; use Line_Draw;
generic
type T is abstract tagged private;
package Make_Printable is
type Printable_T is abstract new T and Printable with private;
procedure Set_Hue(P: in out Printable_T; C: in Colour);
function Hue(P: Printable_T) return Colour;
procedure Set_Width(P: in out Printable_T; W: in Points);
function Width(P: Printable_T) return Points;
private
type Printable_T is abstract new T and Printable with
record
Hue: Colour := Black;
Width: Points := 1;
end record;
end;
This generic can be used to make any type printable.
We simply write
package P_Triangle is new Make_Printable(Triangle);
type Printable_Triangle is new P_Triangle.Printable_T with null record;
function To_Lines(T: Printable_Triangle) return Line_Set;
The instantiation of the package creates a type Printable_T
which has all the hue and width operations and the required additional
components. However, it simply inherits the abstract function To_Lines
and so itself has to be an abstract type. Note that the function To_Lines
has to be especially coded for each type anyway unlike the hue and width
operations which can be the same.
We now do a further derivation largely in order to
give the type Printable_T the required name
Printable_Triangle and at this stage we provide
the concrete function To_Lines.
We can then proceed as before. Thus the generic makes
the whole process very easy – any type can be made printable by
just writing three lines plus the body of the function To_Lines.
Hopefully this example has illustrated a number of
important points about the use of interfaces. The key thing perhaps is
that we can use the procedure Print to print
anything that implements the interface Printable.
Earlier we stated that
it was a common convention to provide pairs of operations to read and
update properties such as Hue and Set_Hue
and Width and Set_Width.
This is not always appropriate. Thus if we have related components such
as X_Coord and Y_Coord
then although individual functions to read them might be sensible,
it is undoubtedly better to update the two values together with a single
procedure such as the procedure Move declared
earlier. Thus if we wish to move an object from the origin (0.0, 0.0)
to say (3.0, 4.0) and do it by two calls
Obj.Set_X_Coord(3.0); -- first change X
Obj.Set_Y_Coord(4.0); -- then change Y
then it seems as if it was transitorily at the point
(3.0, 0.0). There are various other risks as well. We might forget to
set one component or accidentally set the same component twice.
Finally, as discussed earlier, null procedures are
a new kind of subprogram and the user-defined operations of an interface
must be null procedures or abstract subprograms – there is of course
no such thing as a null function.
(Nonlimited interfaces do have one concrete operation
and that is predefined equality; it could even be overridden with an
abstract one.)
Null procedures will be found useful for interfaces
but are in fact applicable to any types. As an example the package Ada.Finalization
now uses null procedures for Initialize, Adjust,
and Finalize as described in the Introduction.
We conclude this section
with a few further remarks on limitedness. We noted earlier that an interface
can be explicitly stated to be limited so we might have
type LI is limited interface; -- limited
type NLI is interface; -- nonlimited
An interface is limited
only if it says limited (or synchronized etc). As mentioned earlier,
a descendant of a nonlimited interface must be nonlimited since it must
implement assignment and equality. So if an interface is composed from
a mixture of limited and nonlimited interfaces it must be nonlimited
type I is interface and LI and NLI; -- legal
type I is limited interface and LI and NLI; -- illegal
In other words, limitedness
is never inherited from an interface but has to be stated explicitly.
This applies to both the composition of interfaces and type derivation.
On the other hand, in the case of type derivation, limitedness is inherited
from the parent provided it is not an interface. This is necessary for
compatibility with Ada 95. So given
type LT is limited tagged ...
type NLT is tagged ...
then
type T is new NLT and LI with ... --legal, T not limited
type T is new NLT and NLI with ... --legal, T not limited
type T is new LT and LI with ... -- legal, T limited
type T is new LT and NLI with ... --illegal
The last is illegal because T
is expected to be limited because it is derived from the limited parent
type LT and yet it is also a descendant of
the nonlimited interface NLI.
In order to avoid certain curious difficulties, Ada
2005 permits limited to be stated explicitly on type derivation.
(It would have been nice to insist on this always for clarity but such
a change would have been too much of an incompatibility.) If we do state
limited explicitly then the parent must be limited (whether it
is a type or an interface).
Using limited
is necessary if we wish to derive a limited type from a limited interface
thus
type T is limited new LI with ...
These rules really all come down to the same thing.
If a parent or progenitor (indeed any ancestor) is nonlimited then the
descendant must be nonlimited. We can state that in reverse, if a type
(including an interface) is limited then all its ancestors must be limited.
An earlier version
of Ada 2005 ran into difficulties in this area because in the case of
a type derived just from interfaces, the behaviour could depend upon
the order of their appearance in the list (because the rules for parent
and progenitors are a bit different). But in the final version of the
language the order does not matter. So
type T is new NLI and LI with ... --legal, not limited
type T is new LI and NLI with ... --legal, not limited
But the following are
of course illegal
type T is limited new NLI and LI with ... -- illegal
type T is limited new LI and NLI with ... -- illegal
There are also similar changes to generic formals
and type extension – Ada 2005 permits limited to be given
explicitly in both cases.
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by: