Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

2.3 The prefixed notation

As mentioned in the Introduction (see Section 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
T is a tagged type,
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). The parameter Y need not be simply the name of an object. It can be anything allowed as a parameter such as a dereference or a function call. But the type T must be tagged.
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 would have been particularly confusing to permit the notation to apply to access types especially an access type A referring to a tagged type T. If the access type and the tagged type both had the same or similar operations Op then ambiguities or errors could easily arise.
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.
Remember that the prefix does not have to be simply the name of an object such as A_Circle or an implicit dereference such as This_One, 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.

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:
Ada-Europe