Object-Oriented Programming
with Ada 9X

Copyright 1993 by S. Barbey, M. Kempe, and A. Strohmeier.
Permission granted only for customary viewing via WWW. All other rights, including reproduction on other electronic media or non-electronic media, reserved.

This is a draft version of a technical report. It is the basis for an article (to be published). This HTML version of the article is provided for viewing in WWW only. Some examples were inspired by the Draft Rationale.


Stéphane Barbey, Magnus Kempe, Alfred Strohmeier

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»


Table of Contents

1. Introduction

2. The structure of Ada 9X

3. Object-oriented programming from Ada 83 to Ada 9X

4. Inheritance

5. Classes and Polymorphism

6. Genericity

7. Visibility and Subsystems

8. Abstract Types

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)

12. Conclusion

13. Bibliography

14. Biographies

Endnotes


1. Introduction

In the Babel tower of programming languages, Ada is known for the quality of its design. Several languages have borrowed from Ada (e.g. exceptions and genericity). At the age of ten, Ada is emerging from its first revision process, where it has acquired "object-orientation". It is natural that the quality, maturity and increased power of the language will attract much attention.

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.

2. The structure of Ada 9X

Programmers familiar with Ada 83 do not need to worry about the future of their existing applications, as Ada 9X is an extension of Ada 83; care has been taken to make Ada 9X upward-compatible with Ada 83.

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 core language contains all Ada 83 and supports new mechanisms:

The specialized needs annexes provide capabilities for specific user communities. They consist mainly of specialized packages, pragmas, and attribute definitions; they may place tougher requirements on an implementation (compiler). They do not introduce new syntax. Their implementation is optional for an Ada 9X compiler; a compiler may claim to comply to only part of an annex, but validation of an annex requires full compliance.

For instance, the Information Systems annex provides for picture-driven text output à la COBOL, and the Numerics annex establishes accuracy and precision requirements.

The availability of those annexes will ensure uniformity of the Ada programming libraries provided with the compilers.

3. Object-oriented programming from Ada 83 to Ada 9X

Object-oriented programming may be described by a symbolic equation as a sum of supporting mechanisms:

Ada 83 already provides for the concepts of objects, for their operations, and for encapsulation in packages. This is sufficient for abstract data types, or for the "class" concept as defined by Wegner in [Weg 87]. Ada 83 completely lacks run-time polymorphism, and has only a limited form of inheritance---with refinement and extension of behavior, but no extension of state. This weak form of inheritance is achieved by type derivation. Although type derivation is not widely used in Ada 83, it is the foundation of the inheritance mechanism in Ada 9X.

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.

4. Inheritance

Inheritance is a means for incrementally building new types from existing ones by keeping, modifying, and adding properties. In Ada 9X, inheritance is based on four mechanisms:

4.1. Type derivation and operation overriding

Derivation is the mechanism used to let a new type (the derived type, also called descendant) inherit the data structure and the operations of some other type (the parent type). The data structure is inherited as such and cannot be modified, except for tagged types, as will be seen. The inheritable operations of a type, called primitive operations of the type, are subprograms which have a formal parameter of the type, a result of the type, or an access parameter(1) designating the type, and which are declared near the type declaration.

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 defined

function "+" (Left, Right: Integer) return Integer;

Each type derived from Integer automatically inherits the function "+", with the implementation of the parent type:

type Length is new Integer;

-- function "+" (Left, Right: Length) return Length;

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.

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;

function "+" (Left, Right: Angle) return Angle;

In this example, a new implementation will be provided for the function "+", taking into account the modular nature of angle addition.

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.

4.2. Type extension

Although type derivation and overriding are by themselves powerful mechanisms, they are not sufficient to fully implement inheritance. They provide for the specialization of the behavior of a type, but do not allow the programmer to specialize the data structure of the type. For that purpose Ada 9X introduces type extension. Type extension is the mechanism used to add components to a data structure. This mechanism is linked to type derivation: the extended type is a new type created by derivation from an existing type and addition of components.

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 with 
null record;
In 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.

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.

4.3. Object Construction

In Ada, objects are not created with a special construct, but are simply declared as belonging to a type. Components may then be initialized either by giving a value to each of them separately or by using the aggregate notation. Here are some examples:

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;
    Figure 1. Creating objects of tagged types.
The extension aggregate is a special form of aggregate for building an object upon another object belonging to the parent type. In the last two assignments, the objects M and W are built upon object H.

4.4. Operations of tagged types

The primitive operations of a tagged type are declared like the primitive operations of any type, with the noticeable exception that a subprogram cannot be a primitive operation of more than one tagged type: there are no multi-methods[Cha 92]. Parameters of tagged types are always passed by reference. In the following example, primitive operations are associated with the types Human, Man and Woman.

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;
    Figure 2. Primitive operations of tagged types.
In this example, the subprograms Christen, Name_of and Title_of are primitive operations of type Human. Therefore, they are inherited by its descendants, Man and Woman. Both Man and Woman override the operation Title_of and keep the implementations of Christen and Name_of. Man adds its own primitive operations Is_Bearded and Shave.

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;
    Figure 3. Calling primitive operations of tagged types.
Note that there is no syntactical distinction between the object for which the operation is primitive and the other parameters in an operation call. Other object-oriented languages often have a prefix notation (e.g. "M.Is_Bearded").

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.

4.5. Type Conversion

For safety's sake, Ada 9X, like Ada 83, has only explicit type conversions. For tagged types, an object can be converted to another type if the target type is an ancestor of the object's type. There are two kinds of type conversions: value and view conversions.

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;
    Figure 4. Value conversions.
The result of a value conversion is an object the component values of which are selected from the converted object, and the tag of which is the tag of the target type. The additional components are lost in the conversion. For instance, when converting from Man to Human, the component Bearded is lost.

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.

5. Classes and Polymorphism

5.1. Derivation classes and run-time type identification

All types derived and extended (directly or indirectly) from a particular tagged type belong to a derivation class rooted at the latter type; thus, a class does not denote a collection of instances, but subsumes an open-ended hierarchy of types, including the union of their sets of values and operations. This definition of class is specific to Ada 9X.

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.

5.2. Polymorphism

From a general point of view, programming languages divide into two groups: untyped and typed languages. Among the latter, strong, static typing is closest to the original purpose of typing, i.e. compile-time diagnosis of programming errors. A statically typed variable denotes objects of exclusively one specific type. However, since all objects in a (derivation) class share some properties---i.e. state and behavior---it is sometimes convenient to handle them in a uniform way. There is therefore a need for some variables to be able to dynamically denote objects of different types, with a concomitant need to find the implementations of operations associated with each type---i.e. there is a need for polymorphism.

The general solution for this need has three ingredients: overloading, class-wide entities, and dispatching (dynamic binding).

5.2.1. Overloading

Overloading is an ad-hoc, static form of polymorphism: one operation name stands for many different, not necessarily related, subprogram implementations. Although they all have the same names, their profiles are different. When an overloaded operation is called, the correct subprogram implementation is chosen at compile-time, i.e. overloading is resolved by static binding of an implementation to an operation call.

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.

5.2.2. Class-wide entities

Instead of being tied to a specific type, variables and formal parameters can be of a class-wide type, or can access (point to) objects in a class-wide type; all these cases correspond to class-wide entities.

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: Human
Display (W); -- W: Woman
Display (M); -- M: Man
Display (P); -- P: Human'Class := ...
With by-reference semantics, it is possible to make a variable cover at run-time a whole class:

type Human_Ref is access Human'Class;
P_Ref: Human_Ref;
P_Ref can designate any object of the class Human, i.e. a Human, Man, Woman, or any of their descendants:

P_Ref := new Man'(First_Name => "John", Bearded => False);
P_Ref := new Woman'(First_Name => "Anne");

5.2.3. Dispatching

In the presence of a class-wide actual parameter, it is generally impossible to select at compile-time the implementation of an overloaded operation. But at run-time, it is possible to dispatch to the implementation according to the tag of the actual parameter; binding of the appropriate code to an operation call is therefore dynamic and can vary for each execution of one same call statement.

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) is
begin
... Title_of (P) ...
-- Dispatching call: the implementation is chosen according to -- the tag of the actual parameter designated by P.
end Display;
Another 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:

declare
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;
A call with a class-wide access parameter is also dispatching:

... Title_of (P_Ref.all) ...
-- P_Ref can designate any object in the class Human.

5.3. Membership operator

It is likely that code varies somewhat with the specific type of some value. The membership operator in may be used in this case:

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;
    Figure 5. Testing membership in a class-wide type.
The operation Is_Bearded can only be applied to an actual parameter P, the specific type of which is in the class Man, thus the membership test. Notice that membership in the subclass Man "P in Man'Class" is used, rather than membership in the specific type Man: "P in Man". This allows to take into account any potential descendant of Man.

5.4. Invocation of shadowed implementations

Very often, the implementation of a primitive operation of a derived type needs to call the overridden implementation of the parent type. Since they are overloaded, the parent's implementation is said to be in the shadow of the descendant's one. In Smalltalk, a call to a shadowed implementation (method) is performed by sending a message to "super".

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) is
begin
Display (Human (M)); -- invocation of the shadowed implementation
Put (", male");
if Is_Bearded (M) then
Put (" is bearded");
end if;
end Display;
A view conversion is used to invoke the shadowed implementation, so a call to Display does not end up in an infinite recursive call.

5.5. Redispatching

A derived type can always override a primitive operation of its parent type, i.e. provide its own implementation. In the presence of classes, care must be taken to distinguish between a call to the parent implementation and a call to the implementation provided by the derived type.

Consider the following implementation of Display (H: Human):

procedure Display (H: Human) is
begin
Put (Title_of (H) & ...); -- ???
end Display;
If 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".

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) is
begin
Put (Title_of (Human'Class (H)) & ...)); -- redispatching
end Display;
In 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.

6. Genericity

So far, we have seen the main mechanisms usually considered to lie at the heart of OOP. We will describe three more elements, not commonly held to be directly related to OOP, but which nevertheless play an important role in the combination of mechanisms offered by Ada 9X in order to support programming by extension (one of the principles behind OOP). It is highly likely that when one will use OOP in Ada, one will also use the three mechanisms examined here, starting with genericity.

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.

6.1. Generic Formal Instances

A generic unit may be instantiated with an instantiation of a generic package as actual parameter. The formal is specified as being an instantiation of a given generic package:

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
    Figure 6. Generic formal instance parameter.
This kind of generic formal parameter simplifies the importation of entire abstractions. It is not necessary to specify a type and a whole enumeration of related subprograms, according to their parameter-result profile; an entire abstraction, such as an abstract data type defined in a generic package, can be imported very simply. This mechanism will usually be a means to build abstractions from abstractions, or extensions of abstractions. For instance one may build on top of a doubly-linked list package (exporting Insert, Delete and Swap) in order to define a Sort operation (applying the insertion sort algorithm).

6.1.1. Illustration: Complex Numbers and Matrices of Complex Numbers

Our illustration provides the specifications of two related Abstract Data Types (ADTs): complex numbers and matrices of complex numbers. The complex numbers ADT is generic with a formal floating-point type. In turn, the complex matrices ADT is generic with an instantiation of the complex numbers ADT as a formal---i.e. with any instantiation of the generic package shown below.

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;
    Figure 7. Generic ADT of complex numbers.
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;
    Figure 8. Generic ADT of complex matrices, parameterized with the ADT of complex numbers.
Clearly, the above is similar to, but much more economical in expression than enumerating both the formal types and all their relevant operations:

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 ...
    Figure 9. Generic ADT of complex matrices, with an exhaustive enumeration of formal parameters.
The following figure shows two typical instantiations of the ADTs for computations with complex numbers and matrices:

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);
    Figure 10. Regular and long float instantiations of complex numbers and matrices.

6.2. Generic Formal Tagged Types

Since we have now two distinct kinds of types ("untagged" vs. tagged), it may sometimes be necessary to distinguish them. This is an instance of tightening the generic contract model: being able to specify more properties about the expected actual generic parameters, e.g. whether a formal type is tagged or not.

generic
type T is tagged private;
package Module_G is ...
    Figure 11. Generic formal tagged type parameter.
Two constructs are of specific interest to tagged types: type extension, allowing the addition of components (this is the basis for "mixin inheritance," cf. 10.2); and class-wide programming, e.g. providing subprograms applicable to T'Class or defining class-wide access types (such declarations would be adapted to any type in a given derivation class, with one instantiation exactly).

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;
    Figure 12. Constructs based on a generic formal tagged type parameter.

6.3. Generic Formal Derived Types

Generic formal derived types have an application similar to generic formal instantiations. Here, the generic formal is defined as derived from a specific ancestor type, meaning that it matches any type in the derivation class rooted at the specified ancestor type. The syntax discriminates between untagged and tagged types:

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
    Figure 13. Generic untagged formal derived type parameter.
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 ...
    Figure 14. Generic tagged formal derived type parameter.
A generic formal derived type parameterizes a generic unit with a type and its primitive operations, though only the ones known from the specified ancestor type. In a similar vein to the case of passing an instantiation of an ADT with a generic formal package instantiation, a set of known operations are here conveniently bundled together with an actual type.(3)

6.3.1. Illustration: ADT of Rational Numbers

Given a tagged ADT for rational numbers, one can provide input-output for these numbers, as well as for any type derived from the ADT (e.g. someone might have optimized the various operators to systematically factor out the common dividers of numerators and denominators). The solution lies in providing a generic input-output package with a formal derived type parameter, the ADT being specified as ancestor type. The necessary operations need not be enumerated as additional formal generic parameters, since they are known from the ancestor type.

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;
    Figure 15. Specification of an ADT for 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;
    Figure 16. Specification of generic I/O for types derived from Rational_Type.
This kind of generic parameter will be especially useful to combine abstractions and to build generic pieces of code adapted to any type within an entire derivation class, on the basis of the properties that apply to all types in the derivation class (i.e. what is known of the root type).

6.4. Other Generic Formals

We have examined the three new important kinds of generic formal parameters, as seen from the OOP perspective. There are a few other new categories of generic formals, not described here; none change the principles behind Ada generics, but they merely ease the use of generics, by helping tighten the contract model (e.g. indefinite types), or integrating the new categories of types in the language (e.g. modular types and abstract types).

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.

7. Visibility and Subsystems

This section deals with the related issues of visibility and subsystems. Again, this is not usually considered as being part of OOP, but it is very important for OO maintenance and programming in the large. Since Ada is required to support long-term maintainability and programming in the large, these issues are dealt with in Ada 9X.

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:

We will also look at control of visibility, i.e. what parts of a module are visible to client modules, and what parts are visible exclusively to a set of strongly related modules.(4) Ada 9X provides fine control over visibility---i.e. how and where to adequately place definitions, in order to declare public, private and internal features (types, objects, components and/or operations of modules).

7.1. Child Library Units

The concept of child library unit is central to the issues of visibility and subsystems. A child unit is bound to a parent library package (the parent unit), and transitively to all parents of the parent unit (the ancestor units). The parent's private declarations are visible to its children, but the parent's body declarations are not; this rule of visibility applies also transitively to the private and body declarations of ancestor units. The parent's private declarations are available within the child's own private part, and thus within the child's body, but not in the public interface of the child; if it were not the case, private declarations of a parent unit could become public through child units, and that would break the parent's abstraction and encapsulation, thus violating the software engineering principle of information hiding.

By contrast, in Ada 83, the library was a flat space of library units.(5)

7.1.1. Separate Extension

With extension through child units, one may add to an existing package, even though one does not change the source code of the original. E.g. one may not be allowed, under configuration management, to change the existing package, maybe in order to avoid extensive regressive testing, or one may not even own its source code.

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.

7.1.2. Illustration: Interface Extension

Given an ADT for message queues, generic traversal can be added to the original ADT, even if one can't touch the original code. This generic traversal must be in a child unit, since it needs to see the private representation of the ADT in order to (efficiently) iterate through it.

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;
    Figure 17. Specification of an ADT for queues of messages.
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);
    Figure 18. Specification of a child unit for the traversal of message queues.
If the full type of Queue_Type was an access type to an incomplete type (with the full type in the body of Message_Queues), the ultimate representation of the queue type would in fact be inaccessible from a child unit. It would be impossible to create any child unit depending on that representation, since a child unit cannot access its parent's body. Thus, it is possible to prevent others from accessing the representation of one's private types by writing child units: use access types to incomplete types (maybe such a representation should be called "dark types"...). This is an instance of a situation where it becomes quite important to distinguish between private and body declarations, as we shall see later.

7.1.3. Illustration: Layers of Abstractions

In order to build an abstraction from an abstraction, the new abstraction needs access to the private declarations of the existing one. For instance, the ubiquitous "widgets" used to program with the X Window System have specializations, such as "labels", which depend for their implementation on the private representation of widgets. In this case, a hierarchy of units will often parallel the hierarchy of abstractions declared in those units.

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;
    Figure 19. A type definition for X Window widgets.
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;
    Figure 20. Labels built on top of widgets, in a child unit.

7.1.4. Illustration: Reducing the Need for Recompilation

Given an ADT for transactions (with the usual operations, such as Start, Commit, and Rollback), services can later be provided by a child unit to audit transactions and produce a log file. The auditing services need visibility on the ADT's private declarations in order to log changes in a transaction, but the original ADT isn't touched.

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;
    Figure 21. Skeleton of an ADT for 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;
    Figure 22. Skeleton of an ADT to audit transactions.
Note that "with"ing a child unit implicitly also "with"s all ancestors of that child unit. Also, a child unit need not "with" its parent, since the visibility rules apply practically as if the specification of the child unit were textually included in that of the parent.

7.2. Visibility

The general problem of visibility may be stated as follows: In large, modular systems, it is necessary to decide and control what can be seen and what cannot be seen from outside a given module (a package, a type or a subsystem). There might be intermediate levels of visibility, e.g. not necessarily excluding visibility from the rest of the system, but allowing a few, select group of units in the system to partake in the visibility of some components or operations of a module.

7.2.1. Levels of Visibility

The problem is similar whether we consider types, packages, or classes. Some properties may be either: public, i.e. visible to anyone; private, i.e. not visible to just anyone, but only to a select circle of "friends" or descendants, the child units; or internal, i.e. not visible to anyone else, not even to children. An analogy would be that what is in front of one's house is public (anyone walking in the street may see it), what is in one's house is private (only those who live in the house may see it---i.e. the family, children), and what is in one's body is internal (one's heart cannot be seen, not even by one's children).

7.2.2. Means of Control

Thus there are three levels of control: public properties are in the interface (the package specification), private properties are in the hidden part of the interface (the private part of the package specification), and internal properties are in the implementation (the package body). The distinction between private and internal was not necessary in Ada 83; it appears in Ada 9X because of the new concept of hierarchical units and the concomitant need to control levels of visibility.

7.3. Subsystems

A subsystem is a hierarchy of library units. It may have subprograms at the leaves, but will most likely consist of a hierarchy of packages. A subsystem may even be generic, in which case all children must be generic too. A subsystem can be manipulated as a tightly integrated assembly of parts, as one entity. The specification of a subsystem is the hierarchy of specifications of its public units.

7.3.1. Private children

A child unit can be a private child, its use being thus restricted to the bodies of the units in the same hierarchy, as well as being available to both specifications and bodies of other private units in that hierarchy. This additional level of control allows to isolate a subsystem's internal properties in hidden units, shared and known only internally in a hierarchically related group of library units.

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.

7.3.2. Reducing the Need for Recompilation

Separating parts of a subsystem into a parent and its child units allows changes in a child without recompiling either the parent or independent siblings. It is thus possible to control how much of a subsystem is dependent on one particular aspect of the same subsystem. As a consequence, recompilation may be circumscribed and limited to a subset of the subsystem's compilation units, although the subsystem externally behaves and may be considered as a single library unit.

7.3.3. Control of the Namespace

Hierarchical units provide more control over the namespace. Consider the case in which one has bought two families of software components; one from Mr. Usable, who has written a package Strings, for string handling; another from Mr. Reusable, which include a similar package also named Strings. In this situation, if one wants to use both families of components at the same time, there is a clash in the library namespace. If, on the other hand, prefixed families of components are provided---e.g. Usable.Strings and Reusable.Strings---then name clashes are easily avoided, since we can concentrate on managing the namespace for subsystems or families of components instead of managing a flat namespace for all of the compilation units.

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).

7.3.4. Illustration: Multiple Views of an Abstraction

Given a complex abstraction encapsulated in a subsystem, one may provide several specialized views of that abstraction through various interface packages of a subsystem. This approach is related to views in database systems.

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.

7.3.5. Illustration: Subdividing Large Interfaces

Consider an operating system (e.g. POSIX): nobody wants to have one huge package with hundreds of types and operations. There must be a logical grouping of types, and of operations; in Ada 9X, the fact that some private types must be shared is no longer a hindrance to such division of operations, thanks to the visibility that child units have over the private declarations of their ancestors. Hierarchical units allow for logical structures and groupings, regardless of shared private types (which are often implementation details that happen to pop up in the private part).

7.3.6. Subcontracting

A subsystem might have a simple, small interface but a very complex, large implementation. Once the specification of the subsystem (one or more package specifications) has been defined, one team might use the subsystem while another (a subcontractor) is working on its implementation.

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.

8. Abstract Types

This is the last of the Ada 9X mechanisms we introduce in this paper, before moving on to see how to apply them with appropriate programming techniques.

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;
    Figure 23. An abstract type declaration, and a concrete type derived from that abstract type.

8.1. Abstract Subprograms

Abstract subprograms are related to abstract types: they have no implementation. Some, not necessarily all, primitive operations associated with abstract types may be abstract, i.e. may have a specification without implementation. An abstract subprogram may not be called (this is checked at compile-time). An abstract type need not have any abstract operation; in such a case, its main purpose is to simply be the root of a derivation class.

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;
    Figure 24. An abstract type declaration with an abstract operation.
Concrete types may not have any abstract primitive operations; abstract operations of an abstract type must therefore be overridden for any concrete type derived from that abstract type. In essence, an abstract subprogram acts as a template for the overriding subprogram that must be declared for concrete derived types.

type C is new A
with record
I : Integer;
end record;
procedure Move (X : C);
    Figure 25. A concrete type derived from the abstract type declared above, with operation overriding.

8.1.1. Illustration: Abstract and Concrete Shapes

We define an abstract type of two-dimensional shapes, and with the following property: a shape has a perimeter of some length. The private declaration indicates that the representation of shapes is totally unknown.

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;
    Figure 26. Abstract definition of a two-dimensional shape.
Building on top of the abstract shape type, a concrete circle is a shape, with a concrete radius, and thus with a concrete perimeter subprogram.

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;
    Figure 27. Specification and implementation of a concrete circle shape.
An abstract type may be thought of as a factoring out of commonalities between a group of types, the factored commonalities being too abstract to allow any meaningful creation of objects merely on the basis of such commonalities. Thereafter, the abstract type must be extended in order to have a concrete, directly usable type.

8.2. Example: Abstract Stacks

For instance, an abstract stack can be defined as a data structure with three operations, Push, Pop and Size, without presuming any specific implementation. This specification may be implemented by various concrete stack types (bounded or unbounded, managed or unmanaged, concurrent or sequential, ...).

8.2.1. Specification of an Abstract Stack

The specification of an abstract stack is similar to the usual stack component we know from Ada 83, except that the stack is an abstract type, and Push and Pop are abstract subprograms. In our example, the partial representation of stacks records their cardinality (the number of elements currently stored), and thus the subprogram Size can be implemented.

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;
    Figure 28. Specification of an abstract stack type.
The body of Stacks_G (not shown here) implements only the operation Size.

8.2.2. Specification of a Concrete Bounded Stack

A concrete bounded stack type is derived in a child unit, because the private declarations of Stacks_G must be visible. Both Push and Pop must be overridden for the concrete stack type. The data representation is exactly the same as what one would write today with Ada 83 to represent a bounded stack with an array.

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;
    Figure 29. Specification of a concrete bounded stack type derived from an abstract stack type.

8.3. Abstract Types are like Generics

A concrete type derived from an abstract type must provide the dispatching operations defined for that abstract type. There is a parallel between generics and abstract types. A generic unit must be instantiated with actual parameters in order to have a directly usable unit, i.e. a package or subprogram. An abstract type must be derived with overriding, concrete subprograms and an extension part, in order to have a directly usable type, i.e. a concrete type with which to declare objects.

8.3.1. Putting the Abstract Stack to Work

We show below how to instantiate Stacks_G with integers, thus producing an abstract type "stack of integers". Then, Stacks_G.Bounded_G is instantiated in order to have a concrete type.

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;
    Figure 30. Instantiation of a bounded stack of integers, and use thereof.

9. Heterogeneous Data Structures
(Collecting polymorphic items)

9.1. By-Reference Semantics (Enhancements to Access Types)

Unlike Eiffel, Smalltalk, and many other object-oriented programming languages, Ada 9X, like Ada 83, encourages by-value semantics. Its philosophy is to work with objects rather than with references to objects. However, the user can deal with by-reference semantics using access types.

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;
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
Note that formal parameters belonging to a tagged type are inherently aliased, i.e. the attribute 'Access is applicable.

9.2. Heterogeneous Containers

It is possible to safely build a collection of objects not having the same (specific) type, but belonging to the same class, i.e. a heterogeneous collection. This can be achieved with an access-to-class-wide type, that designate objects of a class rather than objects of a specific type. In the last example (§ 9.1), values of the type Woman_Ptr can only designate objects of the specific type Woman, ruling out potential descendants. To the contrary of variables of a class-wide type, that cannot change their tag, variables of an access-to-class-wide type can designate any object in the specified class, and can designate different objects during the execution of the program, as long as the object belongs to the specified class.

type Human_Ref is access all Human'Class;
P: Human_Ref := new Man' (First_Name => "John", Bearded =>False);
-- P designates a Man
...
P := Mary'Access;
-- (see above) P designates a Woman
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.

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;
    Figure 31. Heterogeneous array containg objects of the class Human
All primitive operations defined for the root type of the derivation class are of course directly available for all elements put in such heterogeneous collections (in this example Title_of and Name_of). Calls to such operations are dispatching. Operations specific to a subclass (e.g. Shave only applies to Man and its descendants) can be called using the appropriate view conversion (see § 4.5).

9.3. Heterogeneous Input-Output

Besides the capacity to handle heterogeneous data structures, Ada 9X supply streams, a facility to handle sequences of heterogeneous elements, i.e. elements comprising values from possibly different types.

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'Input 
(Stream: access Ada.Streams.Root_Stream_Type'Class)
return T'Class;
The 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.

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;
    Figure 32. Heterogeneous input-output for the class Human
Intrinsically, 'Output and 'Input make use of the subprograms 'Write and 'Read, which are automatically provided for every specific type. Similarly to 'Output and 'Input, these two subprograms are defined as type-related attributes. They respectively write and read an object of a specific type to and from a stream. Their default behavior can be overridden by the programmer by means of an attribute definition clause, e.g.:

procedure Complex_Write 
(Stream: access Ada.Streams.Root_Stream_Type'Class;
Stack: Unbounded_Stack_Type);
for Unbounded_Stack_Type'Write use Complex_Write;
Redefining 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).

10. Initialization, Duplication, and Finalization
(or: Managing the life cycle of objects and values)

Ada 9X supports user-defined initialization and finalization of objects and values; this allows precise control over allocation, duplication, and deallocation of data storage. This control over the life cycle of both objects and their values makes it possible to safely redefine the assignment operation. User-defined control is directly available for types in two predefined classes, the controlled classes. The abstract root types of these predefined classes are defined as follows:

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;
    Figure 33. Specification of the abstract root types of the controlled classes.
The three control operations (Initialize, Finalize, and Split) are automatically called according to specific events in the life cycle of the controlled objects and, in the case of nonlimited types, of their values. Each operation is expected to be overridden for the user-defined controlled types.

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;
    Figure 34. Specification and implementation of a privately controlled human type.
The principle of the code in figure 34 is that each time a Human object is created without initial value, the class variable Number_of_Humans is incremented, and each time a Human object is destroyed, the class variable is decremented.

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
    Figure 35. Behind the stage of the controlled human type.

11. Combining Abstractions (Multiple Inheritance)

Multiple inheritance is a means to create an abstraction by inheriting from two or more abstractions. Although, like single inheritance, multiple inheritance is intended for classifying concepts, its use varies from one language to another. It is often used as a makeshift mechanism to fill in gaps in a language, rather than to create complex type hierarchies. In practice, genuine uses of multiple inheritance are very rare.

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.

11.1. Multiple inheritance for non-classification purposes

Ada 9X offers a variety of constructs apart from derivation; it does not need to use multiple inheritance for non-classification purposes, as apposed to languages where classes are the only form of module, and inheritance is the preeminent mechanism to combine abstractions and to manage the namespace.

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.

11.2. Mixin Inheritance

Mixin inheritance [BC 90] is a form of multiple inheritance in which one of the two combined abstractions is a subpattern. A subpattern, also called pattern of subclassing, is a given set of properties (components and operations) which may arise across several, otherwise unrelated, types, but which are by themselves insufficient to define more than an abstract type.

The programming of subpatterns may be achieved without explicit multiple inheritance by using single inheritance and the new generic capabilities of the language:

Mixing is accomplished by extending the actual tagged type with the abstract derivative, when instantiating the mixin package. With multiple inheritance, the abstract derivative becomes, in a reversal of the inheritance hierarchy, an (abstract) parent type to inherit from; this is called mixin inheritance.

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;
    Figure 36. Specification of the mixin package Graduate_G
When instantiating this package with a tagged type, a new type is created by derivation from the type given at instantiation. The new type has the desired properties.

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
    Figure 37. Mixing the type Woman with the mixin type Graduate
The properties of both the parent type (here Woman) and the mixin type (Graduate) are available for Graduate_Woman.Graduate.

11.2.1. Constrained Mixins

The parent type of the mixin (the generic formal type) can also be constrained to a set of types belonging to a derivation class, to ensure that is has a number of properties that are needed to implement the mixin type, or to ensure the consistency of the mixed types. For instance, it would be meaningless to create a graduate stack.

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;
    Figure 38. Constraining the mixin Graduate to the derivation class Human
As the properties of the parent type are known, the constrained mixin can redefine them if needed. For instance, the function Title_of is overridden to provide a more appropriate implementation.

11.3. Sibling Inheritance (Multiple classification)

In addition to the aforementioned techniques, a somewhat ad-hoc implementation technique of sibling inheritance [Tem 93] (with access discriminants) is provided to handle the case when a type is truly a derivative of more than one parent type, or when clients of the type want to "view it" as an heir of any of its parents.

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.

    Figure 39. Conceptual multiple inheritance and sibling inheritance
For instance, we will show how to create a conceptual type "controlled humans", derived from the standard type Controlled and a slightly modified type Human. The component First_Name is an access to a string rather than a fixed length string.(9) Name can hence designate dynamically created strings of any length. We use the controlled capabilities to de-allocate the storage associated with a First_Name whenever an object of type Human is destroyed.

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;
    Figure 40. Modified specification of the type Human
To achieve the combination of those two types, two "sibling" types, Human_Sibling and Controlled_Sibling are defined, by respectively deriving from Human and Limited_Controlled. These two types together form the conceptual type "controlled human".

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;
    Figure 41. Specification of the conceptual type controlled humans
These types are interrelated, to refer from one of the twins to the other:

These links are of different natures. To_Controlled_Sibling is a "membership link": an object of type Controlled_Sibling is enclosed in each object of type Human_Sibling. The other link, To_Human_Sibling, is a "reference link": To_Human_Sibling designates a Human_Sibling.

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:

    Because To_Controlled_Sibling is a component of Human_Sibling, an object of the type Controlled_Sibling is automatically created each time an object of type Human_Sibling is created. The link To_Controlled_Sibling is automatically initialized to refer to the enclosing object, because the attribute `Access applied to the name of a record type inside its declaration automatically designates the current instance of the type.
    As a consequence, the declaration
      CH: Human_Sibling;
    automatically declares an object of the conceptual type "controlled human".

    Primitive operations can be overridden by the sibling type that inherits them (e.g. Finalize). For instance, to prevent shortages of memory, we will override Finalize to automatically de-allocate the memory used by First_Name when Human becomes inaccessible.
    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;
    Figure 42. Overriding the primitive operation Finalize
    Components of each twin objects can be selected and used by every twin object., using the links to reference them if needed. For instance, the operation Finalize (defined for Controlled_Sibling) makes use of the component First_Name (defined for Human_Sibling).
    Primitive operations can be called in the same fashion.
    New properties, components and operations can easily be added to the conceptual type by extending either of the sibling types. To preserve encapsulation, the sibling types could also be defined as private extension.
    The conceptual type could in turn serve as a parent for another type. It is sufficient to derive the sibling type Human_Sibling, which has a component of the other sibling type. The properties of the other sibling will also be available to the derived type through the link To_Human_Sibling.
    The membership of an object to either of its parent type is tested by using the convenient (class-wide) view of either twin objects.
    declare
      CH: Human_Sibling;  -- simultaneous object generation
    begin
    ... CH in Human'Class ... -- True
    ... CH.To_Controlled_Sibling.all
    in Limited_Controlled'Class ...-- True
    end;
    Figure 43. Testing the membership to either parent types.
    An object of the conceptual type can be assigned to objects of both its parent types in a similar fashion, by selecting the sibling of the conceptual object that matches the type of the desired parent using a view conversion, and assigning this sibling to the target of the assignment.
This model can easily be extended to handle multiple inheritance of more than two types.

12. Conclusion

"Ada was originally designed with three overriding concerns: program reliability and maintenance, programming as a human activity, and efficiency. This revision to the language was designed to provide greater flexibility and extensibility, additional control over storage management and synchronization, and standardized packages oriented toward supporting important application areas, while at the same time retaining the original emphasis on reliability, maintainability, and efficiency."
--- Ada 9X Reference Manual, Design Goals

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


13. Bibliography

[Ada 90]
Ada 9X Requirements (Ada 9X Project Report). Office of the Under Secretary of Defense for Acquisition, Washington, D.C. 20301, December 1990.
[Ada 93]
Programming Language Ada: Language and Standard Libraries (Draft Version 4.0). ISO/IEC CD 8652. Ada 9X Mapping/Revision Team, Intermetrics, Inc., 733 Concord Avenue, Cambridge, Massachusetts 02138, MA, USA, September 1993.
[BC 90]
Gilad Bracha and William Cook. Mixin-Based Inheritance. In Norman Meyrowitz (ed.), OOPSLA/ECOOP'90 Conference Proceedings, Ottawa, Canada, ACM SIGPLAN Notices, 25(10):303--311, October 1990.
[Boo 83]
Grady Booch. Software Engineering with Ada. Benjamin/Cummings, 1983 (1st ed.), 1987 (2nd ed.).
[Cha 92]
Craig Chambers. Object-Oriented Multi-Methods in Cecil. In O. Lehrmann Madsen (ed.), ECOOP'92 Conference Proceedings, Utrecht, The Netherlands, LNCS 615, pp. 33--56. Springer Verlag, June 1992.
[Lif 93]]
Rainer H. Liffers. Inheritance versus containment. ACM Sigplan Notices, 28(9):36--38, September 1993.
[Tem 93]
Joseph Templ. A Systematic Approach to Multiple Inheritance Implementation. ACM SIGPLAN Notices, 28(4):61--66, April 1993.
[Weg 87]
Peter Wegner. Dimensions of Object-Based Language Design. In Norman Meyrowitz (ed.), OOPSLA'87 Conference Proceedings, Orlando, Florida, ACM SIGPLAN Notices, 22(12):168--182, December 1987.

14. Biographies

Stéphane Barbey 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 1992. He is a research assistant at the Software Engineering Laboratory of EPFL since 1992. His domains of research include the object-oriented technology, especially the coding and testing phases of the software life cycle.

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.


Endnotes

(1)
Ada uses the term "access" rather than the low-level notion of "pointer".
(2)
Number, sequence and types of the formal parameters, and also type of result for functions.
(3)
In case of an untagged formal derived type, the subprograms called by the generic unit will be those of the specified ancestor type, not those of the actual parameter type. This is consistent with the treatment of generic formal integer types defined in Ada83.
(4)
"Module" is taken in the wide meaning associated with the software engineering principle of modularity. From a structural point of view, it may be a package, an object, a subsystem, a type, or even a hierarchy of types.
(5)
Secondary units could be held to support a very limited form of hierarchy; for instance, separate units might develop into a hierarchy of compilation units, but they are confined to the bodies of root library units.
(6)
These constraints greatly simplify compilation, linking and computation of elaboration orders for generic subsystems and their instantiations.
(7)
The assignment operation is used in assignment statements, explicit initializations, by-copy parameter passing, and a few other conditions (note that splitting does not occur in by-copy parameter passing, since tagged types are always passed by reference).
(8)
The rules for assignment operations are somewhat more complex in the case of overlapping sources and targets, but the principle is the same: an anonymous object is created with the value of the source, split, used as intermediate source, and finalized.
(9)
Note that the interest of this example is mostly academic: it would have been easier either to declare Dynamic_Humanity.Human as a descendant of Finalization.Controlled, or to declare Name as belonging to Ada.Strings.Unbounded.Unbounded_String, a type that defines strings of unbounded lengths with storage control.

For comments, additions, corrections, gripes, kudos, etc. e-mail to:
Magnus Kempe (kempe@di.epfl.ch)
Page last modified: 94-03-03