Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

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
      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
      C: Character;
   end record;
procedure Op3(X: T1);
-- Op1 and Op2 inherited, Op3 added
type T2 is new T with
      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.
   type PT is new and Int2 and Int3 with 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 
   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);
   type Object is abstract tagged
         X_Coord: Float := 0.0;
         Y_Coord: Float := 0.0;
      end record;
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);
   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;
      -- 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;
 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;
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 
    type Printable_Object is abstract new Geometry.Object and Line_Draw.Printable with
         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
      return P.Hue;
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
         A, B, C: Float;
      end record;
   ... -- functions Area, To_Lines etc
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.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;
 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;
   type Printable_Triangle is new Triangles.Triangle and Line_Draw.Printable with
         Hue: Colour := Black;
         Width: Points := 1;
      end record;
   type Printable_Circle is new Circles.Circle and Line_Draw.Printable with
         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.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;
   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;
   type Printable_T is abstract new and Printable with
         Hue: Colour := Black;
         Width: Points := 1;
      end record;
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 ...
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.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe: