Rationale for Ada 2005
2.3 The prefixed notation
As mentioned in the
Introduction (see
1.3.1), the Ada 95 object
oriented model has been criticized for not being really OO since the
notation for applying a subprogram (method) to an object emphasizes the
subprogram and not the object. Thus given
package P is
type T is tagged ... ;
procedure Op(X: T; ... );
...
end P;
then we usually have
to write
P.Op(Y, ... ); -- subprogram first
in order to apply the
operation to an object Y of type T
whereas an OO person would expect to write
Y.Op( ... ); -- object first
Some hard line OO languages such as Smalltalk take
the view that everything is an object and that all activities are operations
upon some object. Thus adding 2 and 3 can be seen as sending a message
to 2 instructing 3 to be added to it. This is clearly an extreme view.
Older languages take
the view that subprograms are dominant and that they act upon parameters
which might be raw numbers such as 2 or denote objects such as a circle.
Ada 95 primarily takes this view which reflects its Pascal foundation
over 20 years ago. Thus if Area is a function
which returns the area of a circle then we write
A := Area(A_Circle);
However, when we come
to tasks and protected objects Ada takes the OO view in which the identity
of the object comes first. Thus given a task Actor
with an entry Start we call the entry by writing
Actor.Start( ... );
So Ada 95 already uses the object notation although
it only applies to concurrent objects such as tasks. Other objects and,
in particular, objects of tagged types have to use the subprogram notation.
A major irritation
of the subprogram notation is that it is usually necessary to name the
package containing the declaration of the subprogram thus
P.Op(Y, ... ); -- package P mentioned
There are two situations when P
need not be mentioned – one is where the procedure call is actually
inside the package P, the other is where we
have a use clause for P (and even that sometimes
does not give the required visibility). But these are special cases.
In Ada 2005 we can
replace
P.Op(Y, ... ); by the so-called prefixed
notation
Y.Op( ... ); -- package P never mentioned
provided that
- Op is a
primitive (dispatching) or class wide operation of T,
- Y is the
first parameter of Op.
The reason there is never any need to mention the
package is that, by starting from the object, we can identify its type
and thus the primitive operations of the type. Note that a class wide
operation can be called in this way only if it is declared at the same
place as the primitive operations of T (or
one of its ancestors).
There are many advantages of the prefixed notation
as we shall see but perhaps the most important is ease of maintenance
from not having to mention the package containing the declaration of
the operation. Having to name the package is often tricky because in
complicated situations involving several levels of inheritance it may
not be obvious where the operation is declared. This happens especially
when operations are declared implicitly and when class-wide operations
are involved. Moreover if we change the structure for some reason then
operations might move.
As a simple example consider a hierarchy of plane
geometrical object types. All objects have a position given by the two
coordinates x and y (this is the position of the centre
of gravity of the object). There will be other specific properties according
to the type such as the radius of a circle. In addition there might be
general properties such as the area of the object, its distance from
the origin and moment of inertia about its centre.
There are a number of ways in which such a hierarchy
might be structured. We might have a package declaring a root abstract
type and then another package with several derived types.
package Root 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;
function Distance(O: Object) return Float;
end Root;
package body Root is
function Distance(O: Object) return Float is
begin
return Sqrt(O.X_Coord**2 + O.Y_Coord**2);
end Distance;
end Root;
This package declares
the root type and two abstract operations Area
and MI (moment of inertia) and a concrete
operation Distance. We might then have
with Root;
package Shapes is
type Circle is new Root.Object with
record
Radius: Float;
end record;
function Area(C: Circle) return Float;
function MI(C: Circle) return Float;
type Triangle is new Root.Object with
record
A, B, C: Float; -- lengths of sides
end record;
function Area(T: Triangle) return Float;
function MI(T: Triangle) return Float;
-- and so on for other types such as Square
end Shapes;
(In the following discussion we will assume that
use clauses are not being used. This is quite realistic because many
projects forbid use clauses.)
Having declared some
objects such as A_Circle and A_Triangle
we can then apply the operations Area, Distance,
and MI. In Ada 95 we write
A := Shapes.Area(A_Circle);
D := Shapes.Distance(A_Triangle);
M := Shapes.MI(A_Square);
Observe that the operation
Distance is inherited and so is implicitly
declared in the package Shapes for all types
even though there is no mention of it in the text of the package Shapes.
However, if we were using Ada 2005 and the prefixed notation then we
could simply write
A := A_Circle.Area;
D := A_Triangle.Distance;
M := A_Square.MI;
and there is no mention of the package Shapes
at all.
A clever friend then
points out that by its nature Distance is
the same for all types so it would be safer to avoid the risk of it getting
changed by making it class wide. So we change the declaration of Distance
in the package Root thus
function Distance(O: Object'Class) return Float;
and recompile our program.
But the Ada 95 version won't recompile. Why? Because class wide operations
are not inherited. So there is only one function Distance
and it is declared in the package Root. So
all our calls of Distance have to be changed
to
D := Root.Distance(A_Triangle);
However, if we had been using the prefixed notation
then there would have been nothing to change.
Our manager might then
read about the virtues of child packages and tell us to restructure the
whole thing as follows
package Geometry is
type Object is abstract ...
... -- functions Area, MI, Distance
end Geometry;
package Geometry.Circles is
type Circle is new Object with
record
Radius: Float;
end record;
... -- functions Area, MI
end Geometry.Circles;
package Geometry.Triangles is
type Triangle is new Object with
record
A, B, C: Float;
end record;
... -- functions Area, MI
end Geometry.Triangles;
-- and so on
This is of course a
much more beautiful structure and avoids having to write Root.Object
when doing the extensions. But, horrors, our assignments in Ada 95 now
have to be changed to
A := Geometry.Circles.Area(A_Circle);
D := Geometry.Distance(A_Triangle);
M := Geometry.Squares.MI(A_Square);
But the lucky programmer
using Ada 2005 can still write
A := A_Circle.Area;
D := A_Triangle.Distance;
M := A_Square.MI;
and have a refreshing coffee (or a relaxing martini)
while we are toiling with the editor.
Some time later the
program might be extended to accommodate triangles that are specialized
to be equilateral. This might be done by
package Geometry.Triangles.Equilateral is
type Equilateral_Triangle new Triangle with private;
...
private
...
end;
This type of course
inherits all the operations of the type Triangle.
We might now realize that the object A_Triangle
of type Triangle was equilateral anyway and
so it would be better to change it to be of type Equilateral_Triangle.
The lucky Ada 2005 programmer will only have to change the declaration
of the object but the poor Ada 95 programmer will have to change the
calls on all its primitive operations such as
A := Geometry.Triangles.Area(A_Triangle);
to the corresponding
A := Geometry.Triangles.Equilateral.Area(A_Triangle);
Other advantages of
the prefixed notation were mentioned in the
Introduction. One is that it unifies the notation for calling a function
with a single parameter and directly reading a component of the object.
Thus we can write uniformly
X := A_Circle.X_Coord;
A := A_Circle.Area;
Of course if we were foolish and had a visible
component Area as well as a function Area
then we could not call the function in this way.
But now suppose we
decide to make the root type private so that the coordinates cannot be
changed inadvertently. Moreover we decide to provide functions to read
them. So we have
package Geometry is
type Object is abstract tagged private;
function Area(O: Object) return Float is abstract;
function MI(O: Object) return Float is abstract;
function Distance(O: Object'Class) return Float;
function X_Coord(O: Object'Class) return Float;
function Y_Coord(O: Object'Class) return Float;
private
type Object is tagged
record
X_Coord: Float;
Y_Coord: Float;
end record;
end Geometry;
Using Ada 95 we would
now have to change statements such as
X := A_Triangle.X_Coord;
Y := A_Triangle.Y_Coord;
into
X := Geometry.X_Coord(A_Triangle);
Y := Geometry.Y_Coord(A_Triangle);
or (if we had not been
wise enough to make the functions class wide) perhaps even
X := Geometry.Triangles.Equilateral.X_Coord(A_Triangle);
Y := Geometry.Triangles.Equilateral.Y_Coord(A_Triangle);
whereas in Ada 2005 we do not have to make any changes
at all.
Another advantage mentioned
in the Introduction is that when using access types explicit dereferencing
is not necessary. Suppose we have
type Pointer is access all Geometry.Object'Class;
...
This_One: Pointer := A_Circle'Access;
In Ada 95 (assuming
that X_Coord is a visible component) we have
to write
Put(This_One.X_Coord); ...
Put(This_One.Y_Coord); ...
Put(Geometry.Area(This_One.all));
whereas in Ada 2005
we can uniformly write
Put(This_One.X_Coord); ...
Put(This_One.Y_Coord); ...
Put(This_One.Area);
and of course this remains unchanged if we make the
coordinates into functions whereas the Ada 95 statements will need to
be changed.
There are other structural changes that can occur
during program development which are much easier to cope with using the
prefix notation. For example, a class wide operation might be moved.
And in the case of multiple interfaces to be described in the next section
an operation might be moved from one interface to another.
It is clear that the prefixed notation has significant
benefits both in terms of program clarity and for program maintenance.
Other variations on the rules for the use of the
notation were considered. One was that the mechanism should apply to
untagged types as well but this was rejected on the grounds that it might
add to rather than reduce confusion in some cases. In any event, untagged
types do not have class wide types so they are intrinsically simpler.
It is of course important
to note that the first parameter of an operation plays a special role
since in order to take advantage of the prefixed notation we have to
ensure that the first parameter is a controlling parameter. Treating
the first parameter specially can appear odd in some circumstances such
as when there is symmetry among the parameters. Thus suppose we have
a set package for creating and manipulating sets of integers
package Sets is
type Set is tagged private;
function Empty return Set;
function Unit(N: Integer) return Set;
function Union(S, T: Set) return Set;
function Intersection(S, T: Set) return Set;
function Size(S: Set) return Integer;
...
end Sets;
then we can apply the
function Union in the traditional way
A, B, C: Set;
...
C := Sets.Union(A, B);
The object oriented
addict can also write
C := A.Union(B);
but this destroys the obvious symmetry and is rather
like sending 3 to be added to 2 mentioned at the beginning of this discussion.
Hopefully the mature
programmer will use the OO notation wisely. Maybe its existence will
encourage a more uniform style in which the first parameter is always
a controlling operand wherever possible. Of course it cannot be used
for functions which are tag indeterminate such as
function Empty return Set;
function Unit(N: Integer) return Set;
since there are no controlling parameters. If a subprogram
has just one parameter (which is controlling) such as Size
then the call just becomes X.Size and no parentheses
are necessary.
Note that the prefix
does not have to be simply the name of an object such as X,
it could be a function call so we might write
N := Sets.Empty.Size; -- N = 0
M := Sets.Unit(99).Size; -- M = 1
with the obvious results as indicated.
© 2005, 2006 John Barnes Informatics.
Sponsored in part by: