Swiss Federal Institute of Technology in Lausanne
Software Engineering Laboratory
EPFL-DI-LGL
CH--1015 Lausanne
Switzerland
e-mail: {Stephane.Barbey, Magnus.Kempe, Alfred.Strohmeier} @ di.epfl.ch
ABSTRACT. Ada 9X, the revised definition of the Ada programming language, supports object-oriented programming. This paper examines the new, object-oriented features of the language, their use, and how they fit into Ada's strong type system and genericity mechanisms. Basic object-oriented mechanisms are covered, such as inheritance and polymorphism. We then show how to combine these mechanisms into valuable programming techniques; topics include programming by extension, heterogeneous data structures, and mixin inheritance.
RÉSUMÉ. Ada 9X, la définition révisée du langage de programmation Ada, intègre la programmation par objets. Cet article examine les nouvelles caractéristiques du langage propres à la programmation par objets, leur utilisation et leur intégration avec le typage fort de Ada et ses mécanismes de généricité. Nous couvrons les mécanismes fondamentaux liés aux objets, tels que l'héritage et le polymorphisme. Nous montrons ensuite comment combiner ces mécanismes en de précieuses pratiques de programmation, comme la programmation par extension, les structures de données hétérogènes, et l'héritage par «mixin».
KEY WORDS. Object-oriented programming, Ada, Programming languages, Mixins
MOTS-CLEFS. Programmation par objets, Ada, Langages de programmation, «Mixins»
3. Object-oriented programming from Ada 83 to Ada 9X
4.4. Operations of tagged types
5.4. Invocation of shadowed implementations
9. Heterogeneous Data Structures
(Collecting polymorphic items)
10. Initialization, Duplication, and Finalization
(or: Managing the life cycle of objects and values)
11. Combining Abstractions
(Multiple Inheritance)
Although Booch [Boo 83] explained how to do object-oriented design with Ada 83 as an implementation language, Ada 83 itself is generally not considered to be object-oriented; rather, according to the terminology of Wegner [Weg 87], it is said to be object-based, since it provides only a restricted form of inheritance and it lacks polymorphism. Both full inheritance and polymorphism are now considered essential for software reuse and for some software development methods.
These shortcomings have been acknowledged by the Ada community; the revision requirements [Ada 90] include a section for programming by specialization/extension: Ada 9X must allow the definition of "new declared entities whose properties are adapted from those of existing entities by the addition or modification of properties or operations."
After three years of intensive work, the Ada 9X Mapping/Revision Team has completed the revised Ada definition for ISO ballot and ANSI canvass. It includes support for object-oriented programming, among other things. The draft of the new Ada Language Reference Manual [Ada 93] was released for public review in September 1993.
The language definition has two main parts: the core language and the specialized needs annexes. This may be expressed by the following symbolic equation:
The availability of those annexes will ensure uniformity of the Ada programming libraries provided with the compilers.
Ada 9X takes advantage of the existing Ada 83 features and extends them with well-integrated object-oriented mechanisms. Hence, the revision process did not lead to a hybrid language: a minimal number of building blocks were added; these additions remain faithful to the philosophy of Ada 83. The expressive power of Ada now equals, or even outmatches, that of other object-oriented languages. However, there are some fundamental differences between Ada 9X and other well-known object-oriented languages, both in syntax and semantics. For instance, no encapsulating "class" construct ---as found in C++ and Simula--- is needed to provide the full power of object-oriented programming, because packages and types ---two Ada 83 concepts--- are sufficient.
The derived type inherits the primitive operations of the parent type by systematic replacement---in each operation's profile(2)---of the parent type with the derived type.
For example, given the following declarations:
type Integer is ...; -- implementation definedEach type derived from Integer automatically inherits the function "+", with the implementation of the parent type:function "+" (Left, Right: Integer) return Integer;
type Length is new Integer;Notice the systematic replacement of type "Integer" by type "Length", and the comment symbol "--" showing that the operation "+" is automatically inherited and that no explicit declaration has to be provided by the programmer.-- function "+" (Left, Right: Length) return Length;
A derived type can, instead of "sharing" the implementation of a parent's operation, redefine it, i.e. provide a new implementation; the operation is then said to be overridden:
type Angle is new Integer;In this example, a new implementation will be provided for the function "+", taking into account the modular nature of angle addition.function "+" (Left, Right: Angle) return Angle;
New operations can be added to a derived type by declaring them like any primitive operation. Of course, these added operations will not be available for the parent type.
No operation of the parent type can be removed from the derived type. Therefore, selective, also called restrictive, inheritance is not allowed.
Extensible types are called tagged types. The qualifier "tagged" emphasizes that every object of a tagged type carries a tag which identifies its type. A tagged type has the form of a record type with the reserved word tagged in its declaration, or is a type derived from a tagged type. Only tagged types can be extended by adding new components.
Here are some tagged type declarations:
type Human is tagged
record
First_Name: String (1..4);
end record;
type Man is new Human with
record
Bearded: Boolean := False;
end record;
type Woman is new Human withIn this example, both types Man and Woman are derived from Human. They both inherit from Human. Man extends Human by adding a new component, Bearded. Woman adds no new component to its parent type; notice the syntax "with null record" used for this purpose. Human, Man and Woman are tagged types.
null record;
In a package specification, it is also possible to declare a private tagged type, in order to hide its data structure:
type Human is tagged private;A private extension adds private components to a type:
type Man is new Human with private;In both cases, the full type declaration is hidden in the private part of the package specification.
declare
H: Human;
M: Man;
W: Woman;
begin
H.First_Name := "Joe "; -- 1. separate component
H := (First_Name => "Joe "); -- 2. aggregate notation
M := (First_Name => "Joe ", Bearded => False);
M := (H with Bearded => False); -- 3. extension aggregate
W := (H with null record);
end;
package Humanity is
subtype Name is String (1..4);
type Human is tagged private;
procedure Christen (H: in out Human; N: in Name);
-- gives a name to a Human
function Name_of (H: Human) return Name;
-- returns the Name of a Human
function Title_of (H: Human) return String;
-- returns an empty string
type Man is new Human with private;
function Is_Bearded (M: Man) return Boolean;
-- returns True if M is bearded and False otherwise
procedure Shave (M: in out Man);
-- shaves a Man
function Title_of (M: Man) return String;
-- returns "Mr"
type Woman is new Human with private;
function Title_of (W: Woman) return String;
-- returns "Ms"
private
... -- Human, Man and Woman as defined in § 4.2
end Humanity;
In the example, the type declarations are grouped in a single package for convenience, but type extensions need not necessarily occur in the package where the parent type is declared. However, a type extension must always occur at the same scope level as its parent type. This way, a derived type cannot disappear before its parent type. (This is a vital property for heterogeneous data structures.)
A primitive operation of a tagged type is called like any other subprogram:
declare
H: Human;
M: Man;
W: Woman;
begin
-- initialization of H, M, W
...
-- inherited operations
Christen (M, "John"); -- calls to implementations
... Name_of (M) ... -- defined for the type Human
-- overridden operations
... Title_of (H) ... -- returns an empty string
... Title_of (M) ... -- returns "Mr"
... Title_of (W) ... -- returns "Ms"
-- added operations
... Is_Bearded (M) ... -- returns True or False
end;
The operations added to a derived type are of course not available for the parent type. The statement "Shave (H);" would result in a compilation error, because this operation is only defined for Man and its potential descendants.
A value conversion is performed in an assignment:
declare
H: Human;
M: Man;
W: Woman;
begin
-- initialization of H, M, W
...
H := M; -- illegal (no implicit type conversion)
H := Human (M); -- legal (value conversion)
M := Man (H); -- illegal (Man is not an ancestor of Human)
M := Man (W); -- illegal (Man is not an ancestor of Woman)
end;
A view conversion provides a different view of an object---as an actual parameter in a subprogram call---but does not affect its content: extra components become invisible, but are not dropped. A view conversion therefore preserves the tag of the object, whereas a value conversion does not. As will be shown later, the main purpose of view conversions is to invoke shadowed implementations, i.e. to call a parent's implementation of an overridden operation. "Operation (Human (M));" calls the operation implemented by Human, whereas "Operation (M);" would call the operation implemented by Man.
A class-wide type is implicitly associated with each tagged type; it denotes the derivation class rooted at the tagged type. A class-wide type doesn't have a proper name, but is denoted by the attribute 'Class applied to the name of a tagged type. Explicitly declared types are called specific types to stress the difference with class-wide types.
In the examples, Human, Man and Woman are three specific types defining three derivation classes, respectively denoted by Human'Class, Man'Class and Woman'Class. The specific types Human, Man and Woman belong to the derivation class Human'Class. Man is the root of the derivation class Man'Class; Man'Class is a subclass of Human'Class. All potential descendants of Man belong to the class Man, but the ancestors and siblings of Man, e.g. Human and Woman, do not.
Each specific type in a derivation class is identified by a tag, held by each object belonging to the type. This information allows to find at run-time the specific implementations of an object's operations, i.e. to identify its specific type.
The general solution for this need has three ingredients: overloading, class-wide entities, and dispatching (dynamic binding).
Operation inheritance is a subtle form of overloading. When deriving a type, the inherited operations overload the primitive operations of the parent type with operations that share profile, but for the systematic replacement of the parent type with the derived one.
In figure 3, the calls Title_of (H), Title_of (M) and Title_of (W) are bound to the subprogram implementations provided by the types Human, Man, and Woman. Note that Name_of is overloaded, even though the implementation is unique.
Class-wide variables provide by-value semantics for classes. A class-wide type declaration does not carry enough information to create an object; therefore, when declaring a variable of a class-wide type, an initial value must always be provided. The initial value will fix the variable's specific type and hence its tag, for the whole life-time of the variable:
P : Human'Class := ...;P is a variable of the class-wide type Human'Class, or in short a variable of the class Human, i.e. P is either a specific Human, Man, or Woman, or some other specific descendant. Its specific type is determined by the obligatory initial value, which may be a dynamically computed expression.
A formal parameter of a subprogram can also be of a class-wide type. Every actual parameter belonging to the class will match with the formal parameter. Such a subprogram is not bound to one specific type but to all types in a given class, and that is why we call it a class-wide subprogram. Here is an example:
procedure Display (P: in Human'Class);Any actual parameter belonging to the class-wide type Human'Class, to the specific type Human, or to any of its descendants (here Man and Woman) will match the formal parameter:
Display (H); -- H: HumanWith by-reference semantics, it is possible to make a variable cover at run-time a whole class:
Display (W); -- W: Woman
Display (M); -- M: Man
Display (P); -- P: Human'Class := ...
type Human_Ref is access Human'Class;P_Ref can designate any object of the class Human, i.e. a Human, Man, Woman, or any of their descendants:
P_Ref: Human_Ref;
P_Ref := new Man'(First_Name => "John", Bearded => False);
P_Ref := new Woman'(First_Name => "Anne");
In Ada 9X, primitive operations of tagged types are potentially dispatching and are therefore called dispatching operations. If it is impossible to bind a call to such an operation at compile-time, it is a dispatching call; a dispatching call has at least one class-wide actual parameter or result type. Occurrence of dispatching is therefore not a property of the operation, but is linked to the presence of at least one class-wide entity, called a controlling operand. This distinction provides for fine tuning, on a call by call basis, between static and dynamic binding.
Dispatching typically occurs in the body of a subprogram with a class-wide parameter:
procedure Display (P: in Human'Class) isAnother case of dispatching is the use of a class-wide variable, and this is an opportunity to stress how different it is from an "ordinary" call:
begin
... Title_of (P) ...
-- Dispatching call: the implementation is chosen according to -- the tag of the actual parameter designated by P.
end Display;
declareA call with a class-wide access parameter is also dispatching:
H1: Human;
H2: Human'Class := ...;
begin
... Title_of (H1) ...
-- Static binding: the compiler knows the specific type of H1.
-- Therefore it can choose an implementation.
... Title_of (H2) ...
-- Dynamic binding: H2 may be a specific Human, Man, or Woman.
-- Thus the compiler cannot select an implementation.
-- The appropriate implementation is chosen at run-time.
end;
... Title_of (P_Ref.all) ...
-- P_Ref can designate any object in the class Human.
procedure Display (P: in Human'Class) is
begin
Put (Title_of (P) & Name_of (P));
if P in Man'Class then -- membership test
Put (", male");
if Is _Bearded (Man'Class (P)) then
Put (" is bearded");
end if;
elsif P in Woman'Class then
Put (", female");
end if;
end Display;
Suppose a procedure Display (H: Human) has been added to type Human. Its (overriding) implementation for the derived type Man could be as follows:
procedure Display (M: Man) isA view conversion is used to invoke the shadowed implementation, so a call to Display does not end up in an infinite recursive call.
begin
Display (Human (M)); -- invocation of the shadowed implementation
Put (", male");
if Is_Bearded (M) then
Put (" is bearded");
end if;
end Display;
Consider the following implementation of Display (H: Human):
procedure Display (H: Human) isIf Man's implementation of Display is called: Display (M), the (parent's) shadowed implementation is called in the first statement: Display (Human (M)). Within the shadowed implementation, Title_of is called with a controlling operand of the specific type Human, and is thus statically bound to the implementation provided by Human. As a result, a Man would get the same title as a Human would, i.e. an empty string, whereas it should be "Mr".
begin
Put (Title_of (H) & ...); -- ???
end Display;
Care must therefore be taken, not to call the primitive operations of Human but to call the operations of the specific type in current use:
procedure Display (H: Human) isIn the body of the implementation for Human, the conversion of the parameter H to the type Human'Class provides redispatching to the correct implementation of Title_of, depending on the tag of H.
begin
Put (Title_of (Human'Class (H)) & ...)); -- redispatching
end Display;
This section does not explain genericity as such, but answers the question: How has genericity changed from Ada 83 to Ada 9X? Essentially, three new kinds of generic formal parameters have been added: instantiations of generic packages, tagged types, and derived types. These three kinds of generic formal parameters are very useful, as we shall see in the later sections and illustrations, when we get to application techniques of the OO mechanisms.
generic
with package P is new P_G (<>); -- the actual parameters to P_G are known
package Module_G is ... -- (as usual) in P_G and (new) in Module_G too
generic
type Float_Type is digits <>;
package Complex_Numbers_G is
type Complex_Type is private;
function Value (Re, Im : Float_Type) return Complex_Type;
function "+" (Left, Right : Complex_Type) return Complex_Type;
... -- other operations
private
type Complex_Type is record
Re, Im : Float_Type;
end record;
end Complex_Numbers_G;
with Complex_Numbers_G;
generic
with package Complex_Numbers is
new Complex_Numbers_G (<>);
use Complex_Numbers;
package Complex_Matrices_G is
type Matrix_Type is
array (Positive range <>, Positive range <>) of
Complex_Type;
function "*" (Left : Complex_Type; Right : Matrix_Type)
return Matrix_Type;
... -- other operations
end Complex_Matrices_G;
generic
type Float_Type is digits <>;
type Complex_Type is private;
with function Value (Re, Im : Float_Type) return Complex_Type is <>;
with function "+" (Left, Right : Complex_Type) return Complex_Type is <>;
... -- other operations
package Complex_Matrices_G is ...
package Complex_Numbers is
new Complex_Numbers_G (Float);
package Complex_Matrices is
new Complex_Matrices_G (Complex_Numbers);
package Long_Complex_Numbers is
new Complex_Numbers_G (Long_Float);
package Long_Complex_Matrices is
new Complex_Matrices_G (Long_Complex_Numbers);
generic
type T is tagged private;
package Module_G is ...
generic
type T is tagged private;
package Module_G is
type NT is new T -- type extension
with record
B : Boolean;
end record;
function Equals (Left, Right : T'Class) -- class-wide subprogram
return Boolean;
type T_Poly_Ref is -- class-wide access type
access T'Class;
end Module_G;
generic -- within Module_G, the known operations of T are
type NT is new T; -- available for objects of type NT with an implicit
package Module_G is ... -- view conversion from T to NT
generic -- within Module_G, the known operations of T indicate
type NT is new T -- the dispatching operations available for objects of type
with private; -- NT; all calls use NT's own implementations
package Module_G is ...
package Rational_Numbers is
type Rational_Type is tagged private;
function To_Ratio (Numerator, Denominator : Integer)
return Rational_Type;
-- raises Constraint_Error if Denominator = 0
function Numerator (Rational : Rational_Type) return Integer;
function Denominator (Rational : Rational_Type) return Positive;
-- the results are not normalized to factor out common dividers
... -- other operations: "+", "-", "*", "/", ...
private
...
end Rational_Numbers;
with Rational_Numbers, Text_IO;
generic
type Num_Type is
new Rational_Numbers.Rational_Type with private;
package Rational_IO is
procedure Get (File : in Text_IO.File_Type; Item : out Num_Type);
procedure Put (File : in Text_IO.File_Type; Item : in Num_Type);
end Rational_IO;
We have seen in this section the added power of genericity that combines with the new OOP mechanisms of the language, providing in particular for generic manipulation of tagged types and generic class-wide programming.
Large systems are normally composed of hundreds or even thousands of parts. It is unlikely that all parts of a system should interact equally with all others; there are groups (or clusters) of modules that interact closely together but not much---or not at all---with the rest of the system (often only through one specific channel). Such a group is identified as a subsystem, and may be assembled and managed in isolation from the rest of the system. Subsystems are a step in the direction of programming in the large.
To support maintainability and the construction of subsystems, there is in Ada 9X a new construct, child library units, leading to a new notion, hierarchical units. It is now possible to define any kind of library unit (subprogram or package) as a child unit of a library package (or any generic unit as a child of a generic library package). This leads to the creation of hierarchies of library units, each descendant having, in its private part and body, visibility to the private declarations of its ancestors. It is analogous to textually including the specifications of child units within the specification of their parent package---while allowing for separate compilation of these specifications and more flexible visibility.
There are six main uses for hierarchical units:
By contrast, in Ada 83, the library was a flat space of library units.(5)
Separate package extension satisfies a requirement for Ada 9X to give the user more control in managing the library, so as to facilitate team development and limit the need for recompilation. Another advantage of separate extension is that such an approach reduces code size, since the compiler/linker will not include code from unreferenced child units, and makes for cleaner elaboration dependencies.
with Messages; use Messages;
package Message_Queues is
type Queue_Type is limited private;
procedure Clear (Queue : in out Queue_Type);
procedure Insert (Queue : in out Queue_Type; Message : in Message_Type);
procedure Remove (Queue : in out Queue_Type; Message : out Message_Type);
private
...
end Message_Queues;
with Messages; use Messages; -- note: the context clause is not "inherited"
generic
with procedure Action (Message : in Message_Type);
procedure Message_Queues.Traverse_G (Queue : in Queue_Type);
with X_Defs; use X_Defs;
package Xtk is
type Widget_Type is tagged private;
procedure Show (W : in Widget_Type);
private
type Widget_Ref is access all Widget_Type'Class;
type Widget_Type is
record
Parent : Widget_Ref;
Class_Name : X_String;
X, Y : X_Position;
Width, Height : X_Dimension;
Content : X_Bitmap;
end record;
end Xtk;
with X_Defs; use X_Defs;
package Xtk.Labels is
type Label_Type is new Widget_Type with private;
procedure Show (L : in Label_Type);
-- needs to access the private declarations of Xtk (e.g. the position of the label)
private
type Label_Type is new Widget_Type
with record
Label : X_String;
Color : X_Color_Type;
end record;
end Xtk.Labels;
Thus transaction clients which do not depend on the auditing services provided by the child unit need not be recompiled, need not be re-linked, and need not even be re-tested.
package Transactions is
type Transaction_Type is limited private;
procedure Start (T : in out Transaction_Type);
procedure Commit (T : in out Transaction_Type);
procedure Rollback (T : in out Transaction_Type);
private
...
end Transactions;
with Text_IO;
package Transactions.Auditing is
type Audit_Type is limited private;
procedure Create (A : in out Audit_Type; T : in Transaction_Type);
procedure Register (A : in out Audit_Type; T : in Transaction_Type);
procedure Log (A : in Audit_Type; T : in Transaction_Type);
private
...
end Transactions.Auditing;
Private children may be added to, changed in, or deleted from a subsystem without any impact on the specification of the subsystem. Thus, clients of the subsystem need not be recompiled when private children are modified. Although private children are internal---they cannot be accessed from the private parts of other children---they provide an additional level of visibility control, since they are shared between bodies of a subsystem.
This structuring of the namespace has already found an application in the predefined Ada environment: all predefined packages are now child units of either Ada, System, or Interfaces (with appropriate library renames for upward compatibility with Ada 83 code).
For instance, a simple account has both a balance and an interest rate. The customer's view is that he may deposit or withdraw money from his account, and learn what the interest rate is. The bank clerk's view is that he may change the interest rates of various accounts. These two views should of course not be united in one interface (the bank doesn't allow the customer to set the interest rates); it is easy to detect whether code written for the customer's view unduly uses the clerk's view.
Note that given two subsystems---e.g. Radar and Motor---there need not be any clashes in the names of "helper packages" defined in different teams for their own subsystems: Radar.Control does not clash with Motor.Control. Interface definition for subsystems thus simplifies team development and helps avoid (some) problems at integration time.
Thus we have seen that hierarchies of units allow one to structure modules into subsystems, to select partial views of a subsystem, to reduce the need for recompilation, to extend existing abstractions, and to build new abstractions closely related to existing ones. Hierarchical units augment and facilitate the reusability of software components.
An abstract type (not to be confused with an abstract data type, i.e. an ADT) provides the specification of a class, without defining any complete implementation in that class; complete implementations must be provided at a later point. An abstract type is an interface with neither full data representation nor complete algorithms. Such an interface can be shared by many different implementations of the same abstraction; implementations are provided by derived types in the derivation class rooted at the abstract type.
By analogy with a package specification, an abstract type is akin to a type specification. The difference is that one doesn't have either knowledge or desire to write the "body" of the abstract type, there being potentially many different implementations satisfying the specification. An abstract type provides a general specification in the form of a type name, associated operation names and profiles, maybe with partial representation of state and partial implementation of behavior.
An abstract type has the keyword abstract in its declaration and must be tagged. No objects can be declared as belonging to such a type, since it is not a type in the sense that Integer, Character, and Boolean are concrete types. An abstract type is a kind of "shape" for types, and concrete types must be provided later on---in the form of type extensions---in order to use the properties defined by the abstract type.
type A is
abstract
tagged null record;
type C is new A
with record
I : Integer;
end record;
Only abstract types may inherit abstract operations without overriding definition. A function returning an object of a tagged type becomes an abstract subprogram when inherited without overriding definition.
type A is
abstract
tagged null record;
procedure Move (X : A) is
abstract;
type C is new A
with record
I : Integer;
end record;
procedure Move (X : C);
package Shapes is
type Shape_Type is
abstract
tagged private;
function Perimeter (Shape : Shape_Type)
return Float is
abstract;
private
type Shape_Type is
abstract
tagged null record;
end Shapes;
package Shapes.Circles is
type Circle_Type is
new Shape_Type
with private;
procedure Set_Radius (Circle : in out Circle_Type; Radius: in Float);
function Perimeter (Circle : Circle_Type)
return Float;
private
type Circle_Type is
new Shape_Type
with record
Radius : Float := 0.0;
end record;
end Shapes.Circles;
package body Shapes.Circles is
Pi : constant := 3.14159_26536;
function Perimeter (Circle: Circle_Type)
return Float is
begin
return 2.0 * Pi * Circle.Radius;
end Perimeter;
end Shapes.Circles;
generic
type Item_Type is private;
package Stacks_G is
type Stack_Type is
abstract
tagged limited private;
procedure Push (S : in out Stack_Type; Item : in Item_Type) is
abstract;
procedure Pop (S : in out Stack_Type; Item : out Item_Type) is
abstract;
function Size (S : Stack_Type) return Natural;
Empty_Structure_Error : exception;
private
type Stack_Type is
abstract
tagged limited record
Card : Natural := 0;
end record;
end Stacks_G;
generic
package Stacks_G.Bounded_G is
type Stack_Type (Max_Size : Positive) is -- note: discriminant added by the
new Stacks_G.Stack_Type -- type extension
with private;
procedure Push (S : in out Stack_Type; Item : in Item_Type);
procedure Pop (S : in out Stack_Type; Item : out Item_Type);
-- function Size is inherited; it is concrete and need not be overridden
Empty_Structure_Error : exception
renames Stacks_G.Empty_Structure_Error;
private
type Item_Array_Type is array (Positive range <>) of Item_Type;
type Stack_Type (Max_Size : Positive) is
new Stacks_G.Stack_Type
with record
Elements : Item_Array_Type (1 .. Max_Size);
Top : Natural := 0;
end record;
end Stacks_G.Bounded_G;
Note here that the child of a generic unit must be generic, and that the instantiation of such a child must be a child of the instantiation of the corresponding parent. As a consequence, an instantiation of a generic subsystem must be done at library level.(6)
with Stacks_G;
package Integer_Stacks is -- abstract specification for stacks of integers
new Stacks_G (Integer);
with Stacks_G.Bounded_G;
package Integer_Stacks.Bounded is -- definition of concrete bounded stacks of integers
new Stacks_G.Bounded_G;
with Integer_Stacks.Bounded;
procedure Client is
type Stack_Type is
new Integer_Stacks.Bounded.Stack_Type (100)
with null;
A_Stack: Stack_Type;
begin
Push (A_Stack, 3);
end Client;
Access types are the Ada equivalent of pointers. However, they are safer than pointers in most other languages because their semantics prohibits the creation of dangling references. Access types have been greatly enhanced in Ada 9X, while maintaining the safe semantics of Ada 83. Whereas values of an Ada 83 access type could only designate objects dynamically created with the operator new, Ada 9X introduces general access types, whose values can designate declared objects, i.e. variables and constants, as well. The modifiers all or constant appear in the declaration of a general access type. The reserved word all indicates that the designated object is a variable, and can be read and modified, whereas constant indicates that the designated object is a variable or a constant, and can only be read. An access value to a declared object is created with the attribute 'Access. Only objects declared as aliased ---the reserved word aliased appears in their declaration--- can be designated by access values.
type Woman_Ptr is access all Woman;Note that formal parameters belonging to a tagged type are inherently aliased, i.e. the attribute 'Access is applicable.
W: Woman_Ptr;
Mary: aliased Woman;
...
W := new Woman' (First_Name => "Anne");
-- W designates a dynamically created object
W := Mary'Access;
-- W designates a declared object
type Human_Ref is access all Human'Class;In this example, P can designate any object of the class Human (Human, Man or Woman). Given this property, it is now easy to build heterogeneous collections, by declaring a collection, e.g. an array, whose components are of an access-to-class-wide type.
P: Human_Ref := new Man' (First_Name => "John", Bearded =>False);
-- P designates a Man
...
P := Mary'Access;
-- (see above) P designates a Woman
declare
type Human_Ref is access all Human'Class;
Carole, Catherine: aliased Woman;
Alfred, Gabriel, Magnus: aliased Man;
Lab : array (1..5) of Human_Ref :=
(Alfred'Access, Carole'Access, Catherine'Access,
Gabriel'Access, Magnus'Access);
begin
-- display the names of all members of the lab and
-- shave all male members (whether they are bearded or not).
for Member in Lab'Range loop
Put (Title_of (Lab (Member).all) & Name_of ( Lab (Member).all));
if Lab (Member).all in Man'Class then
Shave (Man'Class (Lab (Member).all));
end if;
end loop;
end;
A standard package, Stream_IO, provides control over streams through subprograms such as Create, Open, Close,.... The type-related attributes 'Output and 'Input are implicitly defined for every non-limited class-wide type, and can be used to write and read objects to streams.
procedure T'Class'Output
(Stream: access Ada.Streams.Root_Stream_Type'Class;
Item: T'Class);
function T'Class'InputThe attribute 'Output defines a procedure that writes the tag of the object Item followed by its data structure to the stream. The attribute 'Input defines a function that reads a tag, and reads and returns an object of the specific type defined by the tag. The stored representation of the tag is consistent from one execution of an application to another, so that streams can be used to easily implement a low-level persistent objects manager.
(Stream: access Ada.Streams.Root_Stream_Type'Class)
return T'Class;
with Ada.Streams.Stream_IO;
with Humanity; use Humanity;
procedure Stream_Demo is
type Human_Ref is access all Human'Class;
... -- like in previous example
Lab : array (1..5) of Human_Ref := (...);
-- initialized as in previous example
Human_File: Ada.Streams.Stream_IO.File_Type;
procedure Save_Members is
-- write all components of Lab to the stream
begin
Ada.Streams.Stream_IO.Create (Human_File, "Humans.database");
for Member in Lab'Range loop
Human'Class'Output
(Ada.Streams.Stream_IO.Stream (Human_File), Lab (Member).all);
end loop;
Ada.Streams.Stream_IO.Close (Human_File);
end Save_Members;
procedure Display_Members is
-- read all members from the stream and display their titles and names.
begin
Ada.Streams.Stream_IO.Open
(Human_File, Ada.Streams.Stream_IO.In_File, "Humans.database");
while not End_of_File (Human_File) loop
declare
Person: Human'Class :=
Human'Class'Input
(Ada.Streams.Stream_IO.Stream (Human_File));
begin
Put (Title_of (Lab (Person)) & Name_of ( Lab (Person)));
end;
Ada.Streams.Stream_IO.Close (Human_File);
end loop;
end Display_Members;
begin
Save_Members;
Display_Members;
end Stream_Demo;
procedure Complex_WriteRedefining these attributes can prove to be useful to handle complex objects. For instance, they could be redefined to handle an entire data structure (e.g. a stack and its elements).
(Stream: access Ada.Streams.Root_Stream_Type'Class;
Stack: Unbounded_Stack_Type);
for Unbounded_Stack_Type'Write use Complex_Write;
package Finalization is
type Controlled is
abstract tagged null record;
procedure Initialize (Object : in out Controlled);
procedure Finalize (Object : in out Controlled) is abstract;
procedure Split (Object : in out Controlled) is abstract;
type Limited_Controlled is
abstract tagged limited null record;
procedure Initialize (Object : in out Limited_Controlled);
procedure Finalize (Object : in out Limited_Controlled) is abstract;
end Finalization;
The operation Initialize is called after allocating the storage for a controlled object and default initializing its components, unless an explicit initial value is provided. The operation Finalize is called before the object's storage is destroyed, or before overwriting its value in an assignment statement. The operation Split is called as the last step in an assignment operation targetting a controlled object, i.e. after the bit-wise copy is performed; the purpose of splitting in an assignment operation is to generate an appropriate value for the target object based on that copied from the source.(7)
The following is an example of use, where a class variable is updated when an object of the given class is created, when an object is destroyed, before an object receives a new value, and after an object has been assigned a new value.
with Finalization;
package Humans is
type Human is tagged private;
... -- various operations
function Population_Size return Natural;
-- number of created Human objects
private
Number_of_Humans : Natural := 0;
-- class variable
type Human is new Finalization.Controlled with
record
... -- various components
end record;
procedure Initialize (H : in out Human);
procedure Finalize (H : in out Human);
procedure Split (H : in out Human);
end Humans;
package body Humans is
procedure Initialize (H : in out Human) is
begin
Number_of_Humans := Natural'Succ (Number_of_Humans);
end Finalize;
procedure Finalize (H : in out Human) is
begin
Number_of_Humans := Natural'Pred (Number_of_Humans);
end Finalize;
procedure Split (H : in out Human)
renames Initialize;
... -- Population_Size and various operations
end Humans;
To take care of assignment operations, the class variable is decremented before the assignment of a new value to the target (by Finalize), and subsequently incremented when splitting the copied value (by Split, which in this case need merely rename Initialize).(8) These steps ensure that the class variable accurately reflects the number of Human objects in existence, as the commented code below demonstrates:
with Humans; use Humans;
procedure Population is
-- #humans = 0
H1 : Human; -- Initialize (H1) called: #humans = 1
H2 : Human; -- Initialize (H2) called: #humans = 2
begin
H1 := H2; -- first, Finalize (H1) is called: #humans = 1
-- followed by a bitwise copy of H2 into H1: #humans = 1
-- and (last step in the assignment)
-- finally Split (H1) is called; thus: #humans = 2
end Population;
-- Finalize (H2) is called: #humans = 1
-- Finalize (H1) is called: #humans = 0
Moreover, multiple inheritance usually increases the complexity of a language, necessitating the introduction of many rules to handle pathological cases, for instance to take into account repeated ("diamond shaped") inheritance (i.e. a class inherits from several classes which have a common parent).
On these grounds, Ada 9X does not offer built-in multiple inheritance. However, various programming techniques are available to solve problems that require multiple inheritance in other languages.
In the following sections, we will first see how to handle uses of multiple inheritance that have nothing to do with classification, and then how to achieve multiple classification.
For instance, composing objects from other objects is better achieved through composition than by making use of multiple inheritance [Lif 93]. The reference manual of a well-known object-oriented programming language suggests to define a type Apple-Pie by inheriting from the types Apple and Cinnamon. This is a typical misuse of multiple inheritance: the derived type inherits from all the properties of its parents, although usually not all properties are applicable to the derived type. In this example, it makes no sense to derive applepies from apples (e.g. applepies do not grow on trees, and cannot be peeled).
Ada 9X does not need multiple inheritance to manage the namespace either: this control is independent from classes, and managed with context clauses ("with" clauses). For example, one need not use inheritance to gain access to the entities defined in a module (e.g. a set of constants common to the X Window System).
Implementation dependence, inheriting the specification from one type and the implementation from a second type, can be expressed without turning to multiple inheritance. For example, a Bounded_Stack could be constructed by inheriting from an abstract type Stack, which would define the interface (the operations Push and Pop, ...) and a concrete type Array, which would provide the implementation. However, this form of multiple inheritance is only valid if all operations inherited from the implementation type are meaningful for the derived type. In this example, the operation which gives access to any element of the array should be hidden from the users of Bounded_Stack, since only the element at the top of the stack must be accessible to clients. Once again, as shown in § 8.2.2, this is easy to achieve in Ada 9X, and multiple inheritance is not needed. The new abstraction uses inheritance to define its specification, and makes use of composition for its implementation.
The programming of subpatterns may be achieved without explicit multiple inheritance by using single inheritance and the new generic capabilities of the language:
For example, although the set of properties that defines a graduate is well-known. "Graduates" do no exist as such. It is a subpattern that can be applied to the people who have successfully completed a degree. There are graduate humans, men and women, but being a graduate is a property, and is meaningless without being applied to someone. The properties that define graduation can be gathered in the following mixin Graduate_G.
with Degrees; use Degrees; -- exports the type Degree
generic
type Person is tagged private;
package Graduate_G is
-- A Graduate owns a degree
type Graduate is new Person with private;
procedure Give
(G: in out Graduate;
D: in Degree);
function Degree_of
(G: Graduate)
return Degree;
private
type Graduate is
new Person with
record
Given_Degree: Degree;
end record;
end Graduate_G;
package Graduate_Women is new Graduate_G (Woman);
Anne: Graduate_Women.Graduate; -- a graduate woman
D: Degrees.Degree :=...
Christen (Somebody, "Anne"); -- an operation of Woman
Give (Somebody, Degree); -- an operation of Graduate
The constraint on the parent type can be expressed by specifying the derivation class to which the parent type must belong with a generic formal derived type (instead of a formal tagged type). In the following example, only Human and its descendants can be used to instantiate the mixin.
with Degrees; use Degrees;
generic
type Person is new Human with private;
package Graduate_G is
-- A Graduate is a Person who has a degree
type Graduate is new Person with private;
... -- Definitions of Give and Degree as in the previous example
function Title_of (G: Graduate) return String;
-- override function Title_of to return the given title
private
...
end Graduate_G;
To quote Templ, the basic idea behind sibling inheritance is "to express the effect of multiple inheritance by relying on single inheritance only. [...] It essentially consists of the idea to represent instances of a class that inherits from let's say two super classes by a set of related objects".
In terms of types, the idea is to consider a conceptual type C, derived from two types A and B, as a set of types C_A and C_B, respectively derived from A and B (see figure 39). Objects of the conceptual type C are created by generating together a set of objects of the types C_A and C_B. These objects are called sibling or twin objects. They are interrelated such that the set of objects can be handled as if it were only one object, and that any object can access the properties of its siblings. They are also individually accessible, so that a partial view of the set that corresponds to a specific object is easily available.
package Dynamic_Humanity is
type String_Ptr is access String;
type Human is tagged limited
record
First_Name: String_Ptr;
end record;
procedure Christen (H: in out Human; N: in String_Ptr);
-- gives a name to a Human
... -- see figure 2
end Dynamic_Humanity;
with Dynamic_Humanity, Ada.Finalization;
package Controlled_Human is
type Human_Sibling;
-- incomplete type declaration, needed because of the mutual dependency
type Controlled_Sibling (To_Human_Sibling: access Human_Sibling) is
new Ada.Finalization.Limited_Controlled with null record;
-- To_Human_Sibling is the link to Human_Sibling
procedure Finalize (C: in out Controlled_Sibling);
type Human_Sibling is new Dynamic_Humanity.Human with
record
To_Controlled_Sibling: Controlled_Sibling (Human_Sibling'Access);
-- To_Controlled_Sibling is the link to Controlled_Sibling. This
-- component is automatically initialized to the current instance
-- of Human_Sibling (see below)
end record;
-- primitive operations of Human could possibly be overridden
end Controlled_Human;
It is always possible to get a view of either one of the sibling objects through the links.
Although it requires some extra work (such as de-referencing), all the functions required from multiple inheritance are provided by this programming technique:
CH: Human_Sibling;automatically declares an object of the conceptual type "controlled human".
package body Controlled_Human is
procedure Free is new Unchecked_Deallocation (String_Ptr);
procedure Finalize (C: in out Controlled_Sibling) is
-- overrides Finalize inherited from Controlled
begin
Free (C.To_Human_Sibling.all.First_Name);
end Finalize;
end Controlled_Human;
declare
CH: Human_Sibling; -- simultaneous object generation
begin
... CH in Human'Class ... -- True
... CH.To_Controlled_Sibling.all
in Limited_Controlled'Class ...-- True
end;
We have described object-oriented programming with Ada 9X through the study of supporting mechanisms: objects, operations, encapsulation, inheritance, polymorphism. Ada 9X takes advantage of the existing Ada 83 features and extends them with well-integrated object-oriented mechanisms; these extensions remain faithful to the philosophy of Ada 83. The expressive power of Ada now equals, or even outmatches, that of other object-oriented languages, as has been shown with advanced mechanisms (genericity, child units, control of visibility, subsystems, abstract types) and programming techniques (heterogeneous data structures, managing the life cycle of objects and values, mixin inheritance, sibling inheritance).
"We have made some innovations in the design for the Ada 9X OOP facilities. We believe the innovation [...] should provide a discriminator that will make Ada 9X not just a "me-too" OOPL. We believe Ada 9X will be a particularly safe and maintainable OOPL, while still providing all the power and flexibility needed."
-- S. Tucker Taft (1993), main architect of the revision
Magnus Kempe is an active member of the Swiss delegation to the Ada working group of ISO. He graduated from the C.S. Dept. of EPFL in 1989. He is a research assistant at the Software Engineering Laboratory of EPFL since 1991. His area of research is in the foundations of software engineering methods.
Alfred Strohmeier is a Professor of Computer Science at the Swiss Federal Institute of Technology in Lausanne, Switzerland, where he teaches software engineering, software development methodologies, and programming. He directs a group researching object-oriented methodologies in the complete software engineering life-cycle. He has been teaching object-oriented design and Ada (since 1981) in academic and industrial settings. He is a Distinguished Reviewer of Ada 9X and head of the Swiss delegation within ISO IEC/JTC1/SC22/WG9. He was a member of the team that translated the Ada Language Reference Manual from English to French.