Rationale for Ada 2005
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
- 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
record
X_Coord: Float;
Y_Coord: Float;
end record;
function Area(O: Object) return Float is abstract;
function MI(O: Object) return Float is abstract;
end;
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
record
Radius: Float;
end record;
function Area(C: Circle) return Float;
function MI(C: Circle) return Float;
end;
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
begin
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 given in a later chapter (see
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
will be explained in a later chapter – see
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.
At the moment, 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
overriding
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 will be discussed in a later
chapter – see
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
overriding
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.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: