Ada Home
February 3, 1998

Article

Ada 95 Lessons Learned

by Jean-Marie Dautelle
wb@magi.com

To produce "good" Ada 95 code using object-oriented methodology is certainly not obvious. Despite the comprehensive documentation available, there are many pitfalls along the way. This article is the fruit of experience gained through wanderings, trials and failures.

                   There is one problem with people who know everything:
                           They don't learn anything !
                                          Anonymous

Table of Contents:

  1. How to declare a class
    1. Tagged: Not for every class
    2. Controlled or not Controlled?
    3. Never use non-initialized objects
    4. The myth of object creation
    5. Delete or Finalize?
  2. Naming convention.
    1. Names of the Ada package and the associated main class type
    2. Method names
  3. Object Unity
  4. Portability issues
    1. Predefined scalar types
  5. Generic for Parameterized Classes
  6. Beware of Inheritance
  7. Beware of Controlled Objects
  8. Some weaknesses of the language
    1. Access to subprograms
    2. Multiple inheritance

1. How to declare a class

Almost everybody (but not everybody) agrees that a class in Ada 95 corresponds to a package containing a main type. In most of the cases the main type is declared private or with a private extension. Other types may also be declared within a package specification, we will refer to them as regular types (not class), they are used for interfacing purposes (when public) or to define internal data structures (when private).

Example:

     with Angle;
     package Coordinates is
     
        type Object is ...   -- Class type (either private or derived with private extension).
     
        type Geographic is record  --  Regular type (public)
           Latitude : Angle.Radian;
           Longitude : Angle.Radian;
        end record;
     
     end Coordinates;

That is the basics, but still, the programmer has many choices: should one use tagged types? controlled types? ... Let's discuss the details further.

1.1 Tagged: Not for every class

This rule is simple: Do not tag very simple classes such as Distance, Angle, Temperature ...

Example:

     with Scalar;
     package Angle is
     
        type Object is private;
        Nil : constant Object; 
     
        type Radian is new Scalar.Float_15;
        type Degree is new Scalar.Float_15; 
     
        function Radian_Of (The_Angle : in Angle.Object) return Angle.Radian;
        function Set_Radian (To : in Angle.Radian;
                            Affecting : in out Angle.Object);
     
        function Degree_Of (The_Angle : in Angle.Object) return Angle.Degree;
        function Set_Degree (To : in Angle.Degree;
                            Affecting : in out Angle.Object);
     
        function "+" (Left, Right : in Angle.Object) return Angle.Object;
        ... 
     
     private
           
        type Object is record
           In_Radian : Angle.Radian := Angle.Radian'Last;
        end record;
        Nil : constant Angle.Object := (In_Radian => Angle.Radian'Last);
     
     end Angle;

1.2 Controlled or not Controlled?

When a class is derived from Ada.Finalization.Controlled, the call to Initialize, Adjust and Finalize is automatically done by the compiler (See Reference Manual). This is useful in order to avoid memory leaks (the compiler will never forget to delete an object which is no longer used). It is also useful when an object is duplicated: The two copies can be made independent. This feature should be used only if the default behavior of the compiler is not satisfactory. Indeed, once a class is controlled, the programmer is assumed to take over the controlled processing for each of its children. Thus, if a child adds new extensions, the inherited Initialize, Adjust and Finalize will not consider the new extensions and therefore will have to be overloaded (see: Beware of inheritance).

1.3 Never use non-initialized objects

In order to avoid unpredictable behavior when objects not initialized are used, the class type declaration should set default values for each class attribute. Consequently, each class should define a public Nil object, which can be used as a default attribute value within the context of higher level classes declaration. For simple type such as Integer_32 or Float_6, default value corresponding to limit cases can be used instead (Integer_32'Last, Float_6'Last). For enumerated types a default value corresponding to the default case is used.

1.4 The myth of object creation

If a class declares a function 'Create' returning an instance of a class, all the child classes inherit that operation. This can be hazardous, especially if the child class has more attributes than its parent (note: to make programmers pay attention, the inherited function will be abstract unless overridden). Indeed, the inherited 'Create' might not initialize these attributes. Two solutions are proposed in the Ada 95 Guideline in order to prevent the Create from being inherited:

Neither solution is really satisfactory:

The first solution obliges to explicitly de-allocate each instance when it is no longer used. In some cases the de-allocation is not even possible, ie. A := Create (..).all;

The second solution makes automatic code generation difficult. Also, it forces objects to be copied at least once after they are created, which is a performance hit.

Hopefully, in Ada an explicit 'Create' routine is not really necessary. Any instance of a class is automatically created either at its declaration (static) or using dynamic allocation, ie. Instance := new Class.Object. Therefore, only initialization routines have to be defined. If the default initialization is not satisfactory, new initialization routines may be defined (with or without extra parameters). In order for these routines not to be inherited, they will take as main parameter an object of class-wide type ('Class). For simple classes, the 'Set_<<Attribute_Name>>' method may be used in place of 'Initialize'.

Example:

     with Ada.Finalization;
     with Color, Engine, Speed;
     
     package Car is
        
        type Object is new Ada.Finalization.Controlled with private;
        type Reference is access all Object'Class;
        type View is access constant Object'Class;
        Nil : constant Object;
     
        type Size is (Small, Normal, Big, Unknown);
        subtype Valid_Size is Size range Small .. Big;
     
        procedure Initialize (The_Car : in out Car.Object);  -- Optional
        procedure Adjust (The_Car : in out Car.Object);  -- Optional
        procedure Finalize (The_Car : in out Car.Object);  -- Optional
        procedure Initialize (The_Car : in out Car.Object'Class; -- Class-wide
                              With_Color : in Color.Object;
                              With_Size : in Car.Valid_Size;
                              With_Engine : in Engine.Reference);
        procedure Set_Color (To : in Color.Object;
                            Affecting : in out Car.Object);
        procedure Set_Size (To : in Car.Valid_Size;
                           Affecting : in out Car.Object);
        function Color_Of (The_Car : in Car.Object) return Color.Object;
        function Speed_Of (The_Car : in Car.Object) return Speed.Object;
     
     private
        type Object is new Ada.Finalization.Controlled with record
           Size       : Car.Size := Unknown;
           Own_Color  : Color.Object := Color.Nil;
           Own_Engine : Engine.Reference := null;
           ...
        end record;
        Nil : constant Car.Object
          := (Ada.Finalization.Controlled with
                 Size => Unknown, 
                 Own_Color => Color.Nil,
                 Own_Engine => null,
                 ...);
     end Car;
     
     package body Car is
        procedure Initialize (The_Car : in out Car.Object'Class;
                              With_Color : in Color.Object;
                              With_Size : in Car.Valid_Size;
                              With_Engine : in Engine.Reference) is
        begin
            -- Dispatching call, if car is derived, the child's 'Set_Color' is called.
            --
            Set_Color (To => With_Color,       
                       Affecting => The_Car);                              
      
            -- Non-dispatching call, 'Car.Set_Size' is always called.
            --
            Set_Size (To => With_Size, 
                      Affecting => Car.Object(The_Car));
     
            The_Car.Engine := With_Engine;
        end Initialize;
        ...
     end Car;

1.5 Delete or Finalize?

If the classes are controlled, the 'Delete' routine can be advantageously replaced with 'Finalize'. If the class is not controlled, there is no use for 'Delete'. Simple, isn't it?


2. Naming convention

This section intends to be complementary to the Ada 95 Quality and Style Guide (Chapter 3.2). It provides naming rules proper to object-oriented development using Ada 95.

2.1 Names of the Ada package and the associated main class type

We offer three rules for coherent naming of the entities defined when creating a class:

  1. The package is named after the class it represents.
  2. The name of the main class types are all identical (Object, Instance ...)
  3. The name of the main class pointers are all identical (Reference, Handle, View ...)

Example:

     package Angle is
        type Object is private;
        type Reference is access all Object'Class;
        type View is access constant Object'Class;
        Nil : constant Object;
        ...
     end Angle;
     
     generic
        Item ...
     package List is
        type Object is new Ada.Finalization.Controlled with private;
        type Reference is access all Object'Class;
        type View is access constant Object'Class;
        Nil : constant Object;
        ...
     end List;

Rationale:

This naming scheme remains consistent even when parameterized classes are instantiated. Let us look at the following bad naming convention widely spread in the community:

     generic
        ...
     package List_Pkg is
        type List is new ...
     end List_Pkg;
     
     package Fruit_List_Pkg is
       new List_Pkg (...)
The name of the Fruit_List_Pkg main type is not Fruit_List_Pkg.Fruit_List as one would reasonably expect (in a non-generic solution) but a plain 'Fruit_List_Pkg.List'.

2.2 Method Names

We offer four rules:

  1. Method names in combination with their arguments should read like English sentences.
  2. Functions returning a Boolean result should use predicate clauses.
  3. Naming association should be used for methods involving more than one parameter.
  4. The argument designating the object a method is applied upon, should not be named after the class it belongs; it should use a more generic name instead: Affecting, Self ... This rule only applies when naming association has to be used.

Example:

     Insert (The_Item => An_Apple,
             Before_Index => 3,
             Into => The_Fruit_List);

     Merge (Left => My_Fruit_List,
            Right => The_Citrus_Fruits);

     Is_Empty (The_Queue); -- Positional association used.

Rationale:

If rule 4 is not respected the readability of inherited operations can be affected. Let us see the following case where rule 4 is transgressed:

     package List is 
        ... 
        procedure Pop (The_Item : out List.Item;
                       From_List : in out List.Object); -- Use of From_List instead
                                                        -- of From.
     end List;
     
     package Queue is
        ...
        type Object is new List.Object; -- Queue.Object inherits 
                                        -- List's operations.
     end;
     
     with List, Queue;
     procedure Main is
     begin
        ...
        Queue.Pop(
           The_Item => My_Item
           From_List => The_Queue); -- Not very clear, the name specifies 
                                    -- a list but the argument is a Queue ?!
     end Main;

3. Object Unity

Often, objects belonging to the same class have unique identifier (or key) attributes. If these identifiers are specified at the initialization of the object, how can we ensure that identifier duplication is avoided ?

First Solution:

The class itself keeps track of the objects initialized (internal collection). The initialization of an object is done through an Initialize routine (see: The myth of object creation.), therefore the Initialize can check if the identifier is already in use by looking to an internal table containing the identifiers used. The problem with this solution is that it does its job too well. Indeed, it prevents transitory duplication of the objects and independent duplication for backup or simulation purposes.

More flexible solution:

The unity requirement is allocated to an external collection. Instead of allocating the unity requirement to the class itself, it is always possible to allocate this requirement to a collection containing the instances of the class. The check for object duplication is done when the object is inserted into the collection. In a lot of cases this external collection makes sense, therefore, there is no extra work associated with the duplication check.

Example:

Each instance of the class Car contains a plate number that is supposed to be unique. A possible implementation might be:

     package Car is
     
        type Plate_Number is ...
        
        -- The Initialize routine doesn't check if the plate number
        -- is duplicated.
        --
        procedure Initialize(
           The_Car : in out Car.Object'Class
           With_Plate : Car.Plate_Number);
           ... );
        ...
     end Car;
     
     -- A collection is defined.
     --
     package Car_Table is
       new Table(
        Item => Car.Reference;
        Id => Car.Plate_Number;
        ...);
     
     procedure Main is
        ...
     begin
        ...
        Car.Initialize (The_Car => My_Car;
                        With_Plate => My_Plate;
                       ...);
        Car_Table.Add(
           The_Item => My_Car,
           Into => The_Car_Table,
           Using_Policy => Raise_Exception); -- An exception is raised if 
                                             -- the plate number is  
                                             -- duplicated.
        ...
     end Main;

4. Portability issues

4.1 Predefined Scalar types

For predefined types such as Integer and Float the language reference manual guarantees the minimum range or precision, but any implementation can exceed these values. Therefore two different compilers can have distinct range and/or precision for the predefined types Integer and Float. In some cases portability can be affected, ie. my code executes fine with the compiler A which uses 32 bits for Integer, but would fail with the compiler B which uses only 16 bits (minimum required).

Solution:

The solution to the portability issue for predefined scalar types is simple : Do not use them ! Instead define your own types whose ranges and precision are forced.

Example:

Here is a possible definition of your own scalar types:

     package Scalar is 
     
        type Integer_8 is range -2**7 .. 2**7-1;
        subtype Natural_8 is Integer_8 range 0 .. 2**7-1;
        subtype Positive_8 is Integer_8 range 1 .. 2**7-1;
        for Integer_8'Size use 8;
     
        type Integer_16 is range -2**15 .. 2**15-1;
        subtype Natural_16 is Integer_16 range 0 .. 2**15-1;
        subtype Positive_16 is Integer_16 range 1 .. 2**15-1;
        for Integer_16'Size use 16;
     
        type Integer_32 is range -2**31 .. 2**31-1;
        subtype Natural_32 is Integer_32 range 0 .. 2**31-1;
        subtype Positive_32 is Integer_32 range 1 .. 2**31-1;
        for Integer_32'Size use 32;
     
        type Float_6 is digits 6;
        for Float_6'Size use 32;
     
        type Float_15 is digits 15;
        for Float_15'Size use 64;
       
     end Scalar;

5. Generic for Parameterized Classes

In Ada, a parameterized class maps directly to generic package. Some notation allows parameterized class to add new methods (Booch), with others (UML) parameterized classes have to be derived first in order to add (or overload) class methods. Ada generics correspond to that last case.

If you have to modify or to add any specific behavior to your parameterized class, an intermediate package is used for derivation purpose. It's a good idea to adopt a consistent naming convention for these packages (e.g. <<Class_Name>>_Instance). Also, all the exceptions and regular types of the intermediate package are renamed in order to make the extension transparent to the client packages.

Example:

At first, methods defined within 'List' are satisfactory for Employee_List:

     with List, Employee;
     package Employee_List is
       new List (Item => Employee.Object);

Later (development is an incremental process) the method 'Search' is added to the class Employee_List.

     with List, Employee;
     package Employee_List_Instance is
       new List (Item => Employee.Object);
     
     with Employee_List_Instance, Personne, Employee;
     package Employee_List is
        type Object is new Employee_List_Instance.Object with private;
        type Reference is access all Object'Class;
        type View is access constant Object'Class;
        Nil : constant Object;
        ...
     
        -- Rename all regular types and exceptions of Employee_List_Instance.
        --
        First_Of_Empty_List_Error : exception 
           renames Employee_List_Instance.First_Of_Empty_List_Error;
        ...
        
        subtype Length is Employee_List_Instance.Length;
        ... 
        
        -- Add the new method.
        --
        Search_Not_Found_Error : exception;
        procedure Search (The_Employee : out Employee.View;
                          With_Name : in Personne.Name;
                          Within : in Employee_List.Object);
     private
        ...
     end Employee_List;

6. Beware of inheritance

When a class is derived, all the methods primitive to its ancestors are inherited. But, sometimes, some of the inherited methods will not work for the derived class, (ie. They don't take into account new attributes), it is necessary to overload them.

Forgetting to overload a method that should have been, is one of the most common errors, it cannot be found for sure during unit testing, only a thorough inspection of all the inherited methods in the context of the derived class may detect this oversight.

In Ada 95, controlled objects are first citizen candidates for such neglect, any controlled attribute added to a derived class will force to overload the inherited 'Adjust' and 'Finalize' routines (at the minimum, they will have to call the adjustment and finalization for the new attribute).

Example:

Here, the class Position is derived from Coordinates, but the method 'Distance_Between' has to be overloaded to take into account the new altitude component of the position vector.

     package Coordinates is
        type Object is tagged private;
        ...
        function Distance_Between (Left : in Coordinates.Object;
                                   Right: in Coordinates.Object) 
           return Distance.Object;
        ...
     end Coordinates;
     
     package Position is
        type Object is new Coordinates.Object with private;
        ...
        function Distance_Between (Left : in Position.Object;  -- Overload of
                                   Right : in Position.Object) -- Coordinates.Distance_Between
           return Distance.Object;
        ...
     end Position;

7. Beware of Controlled Objects

The implementation of the Finalize and Adjust operations for controlled objects is very delicate and there are numerous pitfalls along the way. Also, debugging is delicate because any exception raised within these procedures during an assignment generates a cryptic 'Program_Error'. Here is a list of hints which should help in implementing bug-free Adjust and Finalize routines:


8. Some weaknesses of the language

8.1 Access to subprograms

The ability to pass subprograms as parameters is one of the nicest features of Ada 95 (it didn't exist in Ada 83). But accessibility rules make it useless within a multi-tasking program. Indeed, access to a local procedure cannot be used as a parameter of a more globally declared procedure (e.g. a class method). In practice, accessibility rules force the use of global variable which makes multi-tasking very delicate. The only alternative is to use generic parameters (parameterized methods), with the inconvenience that parameterized methods are not inheritable.

Example:

The following looks nice, but it will not compile!

     generic
       type Item is private;
     package List is
        type Object is ...
        ...
        type Action is access procedure Action(
           The_Item : in out Item;
           Stop_Iterating : out Boolean);
        procedure Iterate (
           The_Action : Action
           Through : in List.Object);
        ...
     end List;
     
     with List, Student;
     package Student_List is
       new List (Item => Student.Object);
     
     with Student_List;
     function Retrieve_Student (With_Name : Student.Name;
                                From : Student_List.Object) return Student.Object is

        The_Student_Found : Student.Object;

        procedure Find (The_Student : in out Student.Object;
                        Stop_Iterating : out Boolean) is
        begin
           if Student.Name_Of (The_Student) = With_Name then
              The_Student_Found := The_Student;
              Stop_Iterating := True;
           else
              Stop_Iterating := False;
           end if;
        end Find;

     begin
        Student_List.Iterate (The_Action => Find'Access,   -- Compilation Error
                              Through => From);            
        return The_Student_Found;
     end Retrieve_Student;

It is interesting to note that Ada 95 does not provide the 'Unchecked_Access attribute for subprograms (unlike what is available for variables). That would have solved our problem.

Here is an alternative solution using genericity.

     generic
       type Item is private;
     package List is
        ...
        generic
          with procedure Action(
           The_Item : in out Item;
           Stop_Iterating : out Boolean);
        procedure Iterate (Through : in List.Object'Class); -- Wide class necessary,
                                                            -- parameterized methods are
                                                            -- not inheritable.
        ...
     end List;

8.2 Multiple inheritance

Ada 95 doesn't support multiple parents inheritance (at least not simply). It is not a real issue; one may have a problem imagining for example an object which is at the same time let's say an elephant and the picture of an elephant! However, interface inheritance (such as found in the Java language) would be greatly appreciated. The notion of abstract class is already embedded in the language. Why shouldn't a class be allowed to implement several abstract classes?


About the author:
You can reach Jean-Marie Dautelle via e-mail at wb@magi.com

About the article:
This is a summary of all the lessons learned from three years of experience developing object-oriented software using Ada 95 on three different projects.
Do you have a noteworthy Ada 95 lesson? Let's develop a programming and design tradition around Ada 95!


What did you think of this article?

Very interesting
Interesting
Not interesting
Too long
Just right
Too short
Too detailed
Just right
Not detailed enough
Comments:

Page last modified: 1998-02-03