Previous | Next | Table of Contents | Index | Program List | Copyright

11.3 System Structures: Generic Units

Ada's system of types and procedures requires that the type of a procedure's actual parameter always match that of the formal parameter. This means that a procedure or function that needs to do the same thing to values of two different types must be written twice--once for each type. Consider the procedure Exchange:

    PROCEDURE Exchange(Value1, Value2: IN OUT Natural) IS
      TempValue: Natural;
    BEGIN
      TempValue := Value1;
      Value1    := Value2;
      Value2    := TempValue;
    END Exchange;

A procedure to exchange two Float values would have the same sequence of statements, but the type references would be different:

    PROCEDURE Exchange(Value1, Value2: IN OUT Float) IS
      TempValue: Float;
    BEGIN
      TempValue := Value1;
      Value1    := Value2;
      Value2    := TempValue;
    END Exchange;
Obviously, we could modify the first version to give the second version by using an editor. Because we are likely to need the Natural version again, we modify a copy of it. This gives two versions of a procedure which are almost the same; because of overloading, the two can both be called Exchange. Carrying this to its extreme, we could build up a large library of Exchange programs with our editor and be ready for any eventuality. Exchange could even be made to work with array or record structures, because Ada allows assignment for any type.

There is a problem with this approach: It clutters our file system with a large number of similar programs. Worse still, suppose that a bug turns up in the statements for Exchange or in another program with more complexity. The bug will have turned up in one of the versions; the same bug will probably be present in all of them, but we would probably forget to fix all the others! This is, in miniature, a problem long faced by industry: multiple versions of a program, all similar but not exactly alike, all requiring debugging and other maintenance.

Returning to our simple example, it would be nice if we could create one version of Exchange, test it, then put it in the library. When we needed a version to work with a particular type, we could just tell the compiler to use our pretested Exchange but to change the type it accepts. The compiler would make the change automatically, and we would still be left with only a single copy of the procedure to maintain.

It happens that Ada allows us to do exactly this. The solution to this problem is generics. A generic unit is a recipe or template for a procedure, function, or package. Such a unit is declared with formal parameters that are types and sometimes that are procedure or function names. An analogy can be drawn with an unusual recipe for a layer cake: all the elements are there except that the following items are left as variables to be plugged in by the baker:

This recipe was pretested by the cookbook author, but before we can use it for a three-layer yellow cake with marshmallow filling and chocolate icing, we need to (at least mentally) make all the changes necessary to the ingredients list. Only after this instance of the recipe has been created does it make sense to try to make a cake using it.

Generic Type Parameters

Example 11.2

Program 11.3 is a specification for a generic exchange program. This specification indicates to the compiler that we wish ValueType to be a formal parameter. The formal parameters are listed between the word GENERIC and the procedure heading. Writing

    TYPE ValueType IS PRIVATE; 

tells the compiler that any type, including a private one, can be plugged in as the kind of element to exchange. We will introduce more examples of type parameters below.

Program 11.3
Specification for Generic Exchange Procedure

GENERIC

  TYPE ValueType IS PRIVATE;  -- any type OK except LIMITED PRIVATE

PROCEDURE Swap_Generic(Value1, Value2: IN OUT ValueType);
------------------------------------------------------------------------
--| Specification for generic exchange procedure
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------

The body of Swap_Generic appears as Program 11.4. Notice that SwapGeneric looks essentially the same as the Integer and Float versions, except for the use of ValueType wherever a type is required. ValueType is a formal type parameter.

Program 11.4
Body of Generic Exchange Procedure

PROCEDURE Swap_Generic(Value1, Value2: IN OUT ValueType) IS
------------------------------------------------------------------------
--| Body of generic exchange procedure
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------

  TempValue: ValueType;

BEGIN -- Swap_Generic

  TempValue := Value1;
  Value1    := Value2;
  Value2    := TempValue;

END Swap_Generic;
Compiling the specification and the body creates a version of the generic that is ready to be instantiated, or tailored by plugging in the desired type. Here are two instances:
    PROCEDURE IntegerSwap IS NEW Swap_Generic (ValueType => Integer);
    PROCEDURE CharSwap IS NEW Swap_Generic (ValueType => Character);

The notation is familiar; we have used it in creating instances of Text_IO.Enumeration_IO and other generics. Program 11.5 shows how Swap_Generic could be tested and used. The two instantiations above appear in the program.

Program 11.5
A Test of the Generic Swap Procedure

WITH Swap_Generic;
WITH Ada.Text_IO;
WITH Ada.Integer_Text_IO;
PROCEDURE Test_Swap_Generic IS
------------------------------------------------------------------------
--| Test program for Swap_Generic
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------

  X : Integer;
  Y : Integer;

  A : Character;
  B : Character;

  PROCEDURE IntegerSwap IS NEW Swap_Generic (ValueType => Integer);
  PROCEDURE CharSwap IS NEW Swap_Generic (ValueType => Character);

BEGIN -- Test_Swap_Generic

  X := 3;
  Y := -5;
  A := 'x';
  B := 'q';

  Ada.Text_IO.Put("Before swapping, X and Y are, respectively ");
  Ada.Integer_Text_IO.Put(Item => X, Width => 4);
  Ada.Integer_Text_IO.Put(Item => Y, Width => 4);
  Ada.Text_IO.New_Line;

  IntegerSwap(Value1 => X,Value2 => Y);

  Ada.Text_IO.Put("After  swapping, X and Y are, respectively ");
  Ada.Integer_Text_IO.Put(Item => X, Width => 4);
  Ada.Integer_Text_IO.Put(Item => Y, Width => 4);
  Ada.Text_IO.New_Line;
  Ada.Text_IO.New_Line;

  Ada.Text_IO.Put("Before swapping, A and B are, respectively ");
  Ada.Text_IO.Put(Item => A);
  Ada.Text_IO.Put(Item => B);
  Ada.Text_IO.New_Line;

  CharSwap(Value1 => A,Value2 => B);

  Ada.Text_IO.Put("After  swapping, A and B are, respectively ");
  Ada.Text_IO.Put(Item => A);
  Ada.Text_IO.Put(Item => B);
  Ada.Text_IO.New_Line;

END Test_Swap_Generic;
Sample Run
Before swapping, X and Y are, respectively    3  -5
After  swapping, X and Y are, respectively   -5   3

Before swapping, A and B are, respectively xq
After  swapping, A and B are, respectively qx

Generic Subprogram Parameters

Sometimes, a generic recipe needs to be instantiated with the names of functions or procedures. To continue the food analogy, a certain fish recipe can be prepared by either baking or broiling; the rest of the recipe is independent. So the action "desired cooking method" would be a parameter of that recipe.

Example 12.3

Consider the function Maximum from Program 4.7, which returns the larger of its two Integer operands:

    FUNCTION Maximum (Value1, Value2: Integer) RETURN Integer IS
    
      Result: Integer;
    
    BEGIN
    
      IF Value1 > Value2 THEN
        Result := Value1;
      ELSE
        Result := Value2;
      END IF;
    
      RETURN Result;
    
    END Maximum;

We would like to make a function that returns the larger of its two operands, regardless of the types of these operands. As in the case of Generic_Swap, we can use a generic type parameter to indicate that an instance can be created for any type. This is not enough, however. The IF statement compares the two input values: Suppose the type we use to instantiate does not have an obvious, predefined, "greater than" operation? Suppose the type is a user-defined record with a key field, for example? "Greater than" is not predefined for records! We can surely write such an operation, but we need to inform the compiler to use it; when writing a generic, we need to reassure the compiler that all the operations used in the body of the generic will exist at instantiation time. Let us indicate in the generic specification that a comparison function will exist.

Program 11.6 is the desired generic specification. The WITH syntax here takes getting used to, but it works.

Program 11.6
Specification for Generic Maximum Function

GENERIC

  TYPE ValueType IS PRIVATE;
  WITH FUNCTION Compare(L, R : ValueType) RETURN Boolean;

FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType;
------------------------------------------------------------------------
--| Specification for generic maximum function
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------
The body of the generic function, shown as Program 11.7, looks similar to the one just given for Maximum.

Program 11.7
Body of Generic Maximum Function

FUNCTION Maximum_Generic(L, R : ValueType) RETURN ValueType IS
------------------------------------------------------------------------
--| Body of generic maximum function
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------

BEGIN -- Maximum_Generic

  IF Compare(L, R) THEN
    RETURN L;
  ELSE
    RETURN R;
  END IF;

END Maximum_Generic;
An instantiation for Float values might be
    FUNCTION FloatMax IS 
      NEW Maximum_Generic (ValueType=>Float, Compare=> ">");

Notice how the "greater than" operator is supplied. It makes no difference that the generic expected a function and we gave it an operator; after all, an operator is a function. What is important is that the structure of the actual parameter matches the structure of the formal parameter. As long as there is a ">" available for Float (of course there is, in Standard), the instantiation will succeed.

The Ada compiler has no idea what the function Compare will do when the generic is instantiated. It turns out, then, that if we just supply "<" as an actual parameter for Compare, the instantiation finds the minimum instead of the maximum! Program 11.8 shows a total of six instantiations, giving minimum and maximum functions for Integer, Float, and Currency values. All the minimums are called Minimum; all the maximums are called Maximum; this is just the normal Ada overloading principle in action.

Program 11.8
Test of Generic Maximum Function

WITH Ada.Text_IO;
WITH Ada.Float_Text_IO;
WITH Ada.Integer_Text_IO;
WITH Currency; USE Currency;
WITH Currency.IO;
WITH Maximum_Generic;
PROCEDURE Test_Maximum_Generic IS 
------------------------------------------------------------------------
--| Test program for Generic Maximum, using six instances
--| Author: Michael B. Feldman, The George Washington University 
--| Last Modified: September 1995                                     
------------------------------------------------------------------------

  FUNCTION Maximum IS 
    NEW Maximum_Generic (ValueType=>Float,    Compare=> ">");
  FUNCTION Minimum IS 
    NEW Maximum_Generic (ValueType=>Float,    Compare=> "<");

  FUNCTION Maximum IS 
    NEW Maximum_Generic (ValueType=>Integer,  Compare=> ">");
  FUNCTION Minimum IS 
    NEW Maximum_Generic (ValueType=>Integer,  Compare=> "<");
  
  FUNCTION Maximum IS 
    NEW Maximum_Generic (ValueType=>Quantity, Compare=> ">");
  FUNCTION Minimum IS 
    NEW Maximum_Generic (ValueType=>Quantity, Compare=> "<");

BEGIN -- Test_Maximum_Generic

  Ada.Text_IO.Put("Maximum of -3 and 7 is ");
  Ada.Integer_Text_IO.Put(Item => Maximum(-3, 7), Width=>1);
  Ada.Text_IO.New_Line;
  Ada.Text_IO.Put("Minimum of -3 and 7 is ");
  Ada.Integer_Text_IO.Put(Item => Minimum(-3, 7), Width=>1);
  Ada.Text_IO.New_Line(Spacing => 2);
  
  Ada.Text_IO.Put("Maximum of -3.29 and 7.84 is ");
  Ada.Float_Text_IO.Put
    (Item => Maximum(-3.29, 7.84), Fore=>1, Aft=>2, Exp=>0);
  Ada.Text_IO.New_Line;
  Ada.Text_IO.Put("Minimum of -3.29 and 7.84 is ");
  Ada.Float_Text_IO.Put
    (Item => Minimum(-3.29, 7.84), Fore=>1, Aft=>2, Exp=>0);
  Ada.Text_IO.New_Line(Spacing => 2);
  
  Ada.Text_IO.Put("Maximum of 23.65 and 37.49 is ");
  Currency.IO.Put
    (Item => Maximum(MakeCurrency(23.65), MakeCurrency(37.49)));
  Ada.Text_IO.New_Line;
  Ada.Text_IO.Put("Minimum of 23.65 and 37.49 is ");
  Currency.IO.Put
    (Item => Minimum(MakeCurrency(23.65), MakeCurrency(37.49)));
  Ada.Text_IO.New_Line(Spacing => 2);

END Test_Maximum_Generic; 
Sample Run
Maximum of -3 and 7 is 7
Minimum of -3 and 7 is -3

Maximum of -3.29 and 7.84 is 7.84
Minimum of -3.29 and 7.84 is -3.29

Maximum of 23.65 and 37.49 is 37.49
Minimum of 23.65 and 37.49 is 23.65

Generic Array Parameters

An important use for generics, combined with unconstrained array types, is building very general subprograms to deal with arrays. For a generic to be instantiated for many different array types, we need to specify formal parameters for the index and array types.

Example 12.4

Program 11.9 is a specification for a function ArrayMaximumGeneric that returns the "largest" of all the elements in an array, regardless of the index or element type. "Largest" is in quotes because we know already that we can make it work as a minimum-finder as well.

Program 11.9
Specification for Generic Array Maximum Function

GENERIC

  TYPE ValueType IS PRIVATE;  -- any nonlimited type
  TYPE IndexType IS (<>);     -- any discrete type
  TYPE ArrayType IS ARRAY(IndexType RANGE <>) OF ValueType;
  WITH FUNCTION Compare(L, R : ValueType) RETURN Boolean;

FUNCTION Maximum_Array_Generic(List: ArrayType) RETURN ValueType;
------------------------------------------------------------------------
--| Specification for generic version of array maximum finder
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995
------------------------------------------------------------------------
The syntax of the specification for IndexType means "any discrete type is OK as an actual parameter." Recalling that discrete types are the integer and enumeration types and subtypes, this is exactly what we need for the index type of the array. The specification for ArrayType looks like a type declaration, but it is not. Rather, it is a description to the compiler of the kind of array type acceptable as an actual parameter. In this case, the array type must be indexed by IndexType (or a subtype thereof) and have elements of type Valuetype (or a subtype thereof).

The body of ArrayMaximumGeneric can be seen in Program 11.10. You can write a test program for it as an exercise. As a hint, consider the following declarations:

    TYPE FloatVector IS ARRAY(Integer RANGE <>) OF Float;
    TYPE RationalVector IS ARRAY (Positive RANGE <>) OF Rational;

and instantiate the generic as follows:

    FUNCTION Maximum IS
      NEW Maximum_Array_Generic(ValueType=>Float, IndexType=>Integer,
                              ArrayType=>FloatVector, Compare=>">");
    
    FUNCTION Minimum IS
      NEW Maximum_Array_Generic(ValueType=>Rational, IndexType=>Positive,
                              ArrayType=>RationalVector, Compare=>"<");

Program 11.10
Body of Generic Array Maximum Function

FUNCTION Maximum_Array_Generic(List: ArrayType) RETURN ValueType IS
------------------------------------------------------------------------
--| Body of generic array maximum finder
--| Author: Michael B. Feldman, The George Washington University
--| Last Modified: September 1995    
------------------------------------------------------------------------

  Result: ValueType;

BEGIN -- Maximum_Array_Generic

  FOR WhichElement IN List'Range LOOP
    IF Compare(List(WhichElement), Result) THEN
      Result := List(WhichElement);
    END IF;
  END LOOP;

  RETURN Result;
 
END Maximum_Array_Generic;

Exercises for Section 11.3

Self-Check

  1. Review the ADT's we developed in Chapter 10. For which ones could Swap_Generic not be instantiated? How about Maximum_Generic?

Programming

  1. Modify the test program for Swap_Generic ( Program 11.5) to instantiate for some other types.
  2. Repeat problem 1 for Maximum_Generic ( Program 11.8).
  3. Write a test program for Maximum_Array_Generic as suggested in the section.


Previous | Next | Table of Contents | Index | Program List | Copyright

Copyright © 1996 by Addison-Wesley Publishing Company, Inc.