Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

1.3.1 Overview: The object oriented model

The Ada 95 object oriented model has been criticized as not following the true spirit of the OO paradigm in that the notation for applying subprograms to objects is still dominated by the subprogram and not by the object concerned. It is claimed that real OO people always give the object first and then the method (subprogram). Thus given
package P is
   type T is tagged ... ;
   procedure Op(X: T; ... );
end P;
then assuming that some variable Y is declared of type T, in Ada 95 we have to write 
P.Op(Y, ... );
in order to apply the procedure Op to the object Y whereas a real OO person would expect to write something like 
Y.Op( ... );
where the object Y comes first and only any auxiliary parameters are given in the parentheses.
A real irritation with the Ada 95 style is that the package P containing the declaration of Op has to be mentioned as well. (This assumes that use clauses are not being employed as is often the case.) However, given an object, from its type we can find its primitive operations and it is illogical to require the mention of the package P. Moreover, in some cases involving a complicated type hierarchy, it is not always obvious to the programmer just which package contains the relevant operation.
The prefixed notation giving the object first is now permitted in Ada 2005. The essential rules are that a subprogram call of the form P.Op(Y, ... ); can be replaced by Y.Op( ... ); provided that 
T is a tagged type,
Op is a primitive (dispatching) or class wide operation of T,
Y is the first parameter of Op
The new prefixed notation has other advantages in unifying the notation for calling a function and reading a component of a tagged type. Thus consider the following geometrical example which is based on that in a (hopefully familiar) textbook [6] 
package Geometry is
   type Object is abstract tagged
         X_Coord: Float;
         Y_Coord: Float;
      end record;
   function Area(O: Object) return Float is abstract;
   function MI(O: Object) return Float is abstract;
The type Object has two components and two primitive operations Area and MI (Area is the area of an object and MI is its moment of inertia but the fine details of Newtonian mechanics need not concern us). The key point is that with the new notation we can access the coordinates and the area in a unified way. For example, suppose we derive a concrete type Circle thus
package Geometry.Circle is
   type Circle is new Object with
         Radius: Float;
      end record;
   function Area(C: Circle) return Float;
   function MI(C: Circle) return Float;
where we have provided concrete operations for Area and MI. Then in Ada 2005 we can access both the coordinates and area in the same way
X:= A_Circle.X_Coord;
A:= A_Circle.Area;  -- call of function Area
Note that since Area just has one parameter (A_Circle) there are no parentheses required in the call. This uniformity is well illustrated by the body of MI which can be written as
   function MI(C: Circle) is
      return 0.5 * C.Area * C.Radius**2;
   end MI;
whereas in Ada 95 we had to write 
      return 0.5 * Area(C) * C.Radius**2;
which is perhaps a bit untidy.
A related advantage concerns dereferencing. If we have an access type such as 
type Pointer is access all Object'Class;
This_One: Pointer := A_Circle'Access;
and suppose we wish to print out the coordinates and area then in Ada 2005 we can uniformly write 
Put(This_One.X_Coord); ...
Put(This_One.Y_Coord); ...
Put(This_One.Area); ...    -- Ada 2005
whereas in Ada 95 we have to write 
Put(This_One.X_Coord); ...
Put(This_One.Y_Coord); ...
Put(Area(This_One.all)); ...    -- Ada 95
In Ada 2005 the dereferencing is all implicit whereas in Ada 95 some dereferencing has to be explicit which is ugly.
The reader might feel that this is all syntactic sugar for the novice and of no help to real macho programmers. So we shall turn to the topic of multiple inheritance. In Ada 95, multiple inheritance is hard. It can sometimes be done using generics and/or access discriminants (not my favourite topic) but it is hard work and often not possible at all. So it is a great pleasure to be able to say that Ada 2005 introduces real multiple inheritance in the style of Java.
The problem with multiple inheritance in the most general case is clashes between the parents. Assuming just two parents, what happens if both parents have the same component (possibly inherited from a common ancestor)? Do we get two copies? And what happens if both parents have the same operation but with different implementations? These and related problems are overcome by placing firm restrictions on the possible properties of parents. This is done by introducing the notion of an interface.
An interface can be thought of as an abstract type with no components – but it can of course have abstract operations. It has also proved useful to introduce the idea of a null procedure as an operation of a tagged type; we don't have to provide an actual body for such a null procedure (and indeed cannot) but it behaves as if it has a body consisting of just a null statement. So we might have 
package P1 is
   type Int1 is interface;
   procedure Op1(X: Int1) is abstract;
   procedure N(X: Int1) is null;
end P1;
Note carefully that interface is a new reserved word. We could now derive a concrete type from the interface Int1 by 
   type DT is new Int1 with record ... end record;
   procedure Op1(NX: DT);
We can provide some components for DT as shown (although this is optional). We must provide a concrete procedure for Op1 (we wouldn't if we had declared DT itself as abstract). But we do not have to provide an overriding of N since it behaves as if it has a concrete null body anyway (but we could override N if we wanted to).
We can in fact derive a type from several interfaces plus possibly one conventional tagged type. In other words we can derive a tagged type from several other types (the ancestor types) but only one of these can be a normal tagged type (it has to be written first). We refer to the first as the parent (so the parent can be an interface or a normal tagged type) and any others as progenitors (and these have to be interfaces).
So assuming that Int2 is another interface type and that T1 is a normal tagged type then all of the following are permitted 
type DT1 is new T1 and Int1 with null record;
type DT2 is new Int1 and Int2 with
   record ... end record;
type DT3 is new T1 and Int1 and Int2 with ...
It is also possible to compose interfaces to create further interfaces thus 
type Int3 is interface and Int1;
type Int4 is interface and Int1 and Int2 and Int3;
Note carefully that new is not used in this construction. Such composed interfaces have all the operations of all their ancestors and further operations can be added in the usual way but of course these must be abstract or null.
There are a number of simple rules to resolve what happens if two ancestor interfaces have the same operation. Thus a null procedure overrides an abstract one but otherwise repeated operations have to have the same profile.
Interfaces can also be marked as limited. 
type LI is limited interface;
An important rule is that a descendant of a nonlimited interface must be nonlimited. But the reverse is not true.
Some more extensive examples of the use of interfaces will be found in Section 2.4.
Incidentally, the newly introduced null procedures are not just for interfaces. We can give a null procedure as a specification whatever its profile and no body is then required or allowed. But they are clearly of most value with tagged types and inheritance. Note in particular that the package Ada.Finalization in Ada 2005 is 
package Ada.Finalization is
   pragma Preelaborate(Finalization);
   pragma Remote_Types(Finalization);
   type Controlled is abstract tagged private;
   pragma Preeleborable_Initialization(Controlled);
   procedure Initialize(Object: in out Controlled) is null;
   procedure Adjust(Object: in out Controlled) is null;
   procedure Finalize(Object: in out Controlled) is null;
   -- similarly for Limited_Controlled
end Ada.Finalization;
The procedures Initialize, Adjust, and Finalize are now explicitly given as null procedures. This is only a cosmetic change since the Ada 95 RM states that the default implementations have no effect. However, this neatly clarifies the situation and removes ad hoc semantic rules. (The pragma Preelaborable_Initialization is explained in Section 6.4.)
Another important change is the ability to do type extension at a level more nested than that of the parent type. This means that controlled types can now be declared at any level whereas in Ada 95, since the package Ada.Finalization is at the library level, controlled types could only be declared at the library level. There are similar advantages in generics since currently many generics can only be instantiated at the library level.
The final change in the OO area to be described here is the ability to (optionally) state explicitly whether a new operation overrides an existing one or not.
In Ada 95, small careless errors in subprogram profiles can result in unfortunate consequences whose cause is often difficult to determine. This is very much against the design goal of Ada to encourage the writing of correct programs and to detect errors at compilation time whenever possible. Consider 
with Ada.Finalization; use Ada.Finalization;
package Root is
   type T is new Controlled with ... ;
   procedure Op(Obj: in out T; Data: in Integer);
   procedure Finalise(Obj: in out T);
end Root;
Here we have a controlled type plus an operation Op of that type. Moreover, we intended to override the automatically inherited null procedure Finalize of Controlled but, being foolish, we have spelt it Finalise. So our new procedure does not override Finalize at all but merely provides another operation. Assuming that we wrote Finalise to do something useful then we will find that nothing happens when an object of the type T is automatically finalized at the end of a block because the inherited null procedure is called rather than our own code. This sort of error can be very difficult to track down.
In Ada 2005 we can protect against such errors since it is possible to mark overriding operations as such thus 
   procedure Finalize(Obj: in out T);
And now if we spell Finalize incorrectly then the compiler will detect the error. Note that overriding is another new reserved word. However, partly for reasons of compatibility, the use of overriding indicators is optional; there are also deeper reasons concerning private types and generics which are discussed in Section 2.7.
Similar problems can arise if we get the profile wrong. Suppose we derive a new type from T and attempt to override Op thus 
package Root.Leaf is
   type NT is new T with null record;
   procedure Op(Obj: in out NT; Data: in String);
end Root.Leaf;
In this case we have given the identifier Op correctly but the profile is different because the parameter Data has inadvertently been declared as of type String rather than Integer. So this new version of Op will simply be an overloading rather than an overriding. Again we can guard against this sort of error by writing
   procedure Op(Obj: in out NT; Data: in Integer);
On the other hand maybe we truly did want to provide a new operation. In this case we can write not overriding and the compiler will then ensure that the new operation is indeed not an overriding of an existing one thus 
   not overriding
   procedure Op(Obj: in out NT; Data: in String);
The use of these overriding indicators prevents errors during maintenance. Thus if later we add a further parameter to Op for the root type T then the use of the indicators will ensure that we modify all the derived types appropriately.

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: