Generics are used for metaprogramming in Ada. They are useful for
abstract algorithms that share common properties with each other.
Either a subprogram or a package can be generic. A generic is declared
by using the keyword generic. For example:
generic
type T is private;
-- Declaration of formal types and objects
-- Below, we could use one of the following:
-- <procedure | function | package>
procedure Operator (Dummy : in out T);
procedure Operator (Dummy : in out T) is
begin
null;
end Operator;
Formal types are abstractions of a specific type. For example, we may
want to create an algorithm that works on any integer type, or even on
any type at all, whether a numeric type or not. The following example
declares a formal type T for the Set procedure.
generic
type T is private;
-- T is a formal type that indicates that
-- any type can be used, possibly a numeric
-- type or possibly even a record type.
procedure Set (Dummy : T);
procedure Set (Dummy : T) is
begin
null;
end Set;
The declaration of T as private indicates that you can map
any definite type to it. But you can also restrict the declaration to allow
only some types to be mapped to that formal type. Here are some
examples:
We don't repeat the generic keyword for the body declaration of a
generic subprogram or package. Instead, we start with the actual
declaration and use the generic types and objects we declared. For example:
generic
type T is private;
X : in out T;
procedure Set (E : T);
procedure Set (E : T) is
-- Body definition: "generic" keyword
-- is not used
begin
X := E;
end Set;
Generic subprograms or packages can't be used directly. Instead, they
need to be instantiated, which we do using the new keyword, as
shown in the following example:
generic
type T is private;
X : in out T;
-- X can be used in the Set procedure
procedure Set (E : T);
procedure Set (E : T) is
begin
X := E;
end Set;
with Ada.Text_IO; use Ada.Text_IO;
with Set;
procedure Show_Generic_Instantiation is
Main : Integer := 0;
Current : Integer;
procedure Set_Main is new Set (T => Integer,
X => Main);
-- Here, we map the formal parameters to
-- actual types and objects.
--
-- The same approach can be used to
-- instantiate functions or packages, e.g.:
--
-- function Get_Main is new ...
-- package Integer_Queue is new ...
begin
Current := 10;
Set_Main (Current);
Put_Line ("Value of Main is "
& Integer'Image (Main));
end Show_Generic_Instantiation;
In the example above, we instantiate the procedure Set by mapping the
formal parameters T and X to actual existing elements, in this case
the Integer type and the Main variable.
The previous examples focused on generic subprograms. In this section,
we look at generic packages. The syntax is similar to that used for
generic subprograms: we start with the generic keyword and
continue with formal declarations. The only difference is that
package is specified instead of a subprogram keyword.
Here's an example:
generic
type T is private;
package Element is
procedure Set (E : T);
procedure Reset;
function Get return T;
function Is_Valid return Boolean;
Invalid_Element : exception;
private
Value : T;
Valid : Boolean := False;
end Element;
package body Element is
procedure Set (E : T) is
begin
Value := E;
Valid := True;
end Set;
procedure Reset is
begin
Valid := False;
end Reset;
function Get return T is
begin
if not Valid then
raise Invalid_Element;
end if;
return Value;
end Get;
function Is_Valid return Boolean is (Valid);
end Element;
with Ada.Text_IO; use Ada.Text_IO;
with Element;
procedure Show_Generic_Package is
package I is new Element (T => Integer);
procedure Display_Initialized is
begin
if I.Is_Valid then
Put_Line ("Value is initialized");
else
Put_Line ("Value is not initialized");
end if;
end Display_Initialized;
begin
Display_Initialized;
Put_Line ("Initializing...");
I.Set (5);
Display_Initialized;
Put_Line ("Value is now set to "
& Integer'Image (I.Get));
Put_Line ("Reseting...");
I.Reset;
Display_Initialized;
end Show_Generic_Package;
In the example above, we created a simple container named Element,
with just one single element. This container tracks whether the
element has been initialized or not.
After writing package definition, we create the instance I of the
Element. We use the instance by calling the package subprograms
(Set, Reset, and Get).
In addition to formal types and objects, we can also declare formal
subprograms or packages. This course only describes formal subprograms;
formal packages are discussed in the advanced course.
We use the with keyword to declare a formal subprogram. In the
example below, we declare a formal function (Comparison) to be
used by the generic procedure Check.
generic
Description : String;
type T is private;
with function Comparison (X, Y : T) return Boolean;
procedure Check (X, Y : T);
with Ada.Text_IO; use Ada.Text_IO;
procedure Check (X, Y : T) is
Result : Boolean;
begin
Result := Comparison (X, Y);
if Result then
Put_Line ("Comparison ("
& Description
& ") between arguments is OK!");
else
Put_Line ("Comparison ("
& Description
& ") between arguments is not OK!");
end if;
end Check;
with Check;
procedure Show_Formal_Subprogram is
A, B : Integer;
procedure Check_Is_Equal is new
Check (Description => "equality",
T => Integer,
Comparison => Standard."=");
-- Here, we are mapping the standard
-- equality operator for Integer types to
-- the Comparison formal function
begin
A := 0;
B := 1;
Check_Is_Equal (A, B);
end Show_Formal_Subprogram;
Ada offers generic I/O packages that can be instatianted for standard and
derived types. One example is the generic Float_IO package, which
provides procedures such as Put and Get. In fact,
Float_Text_IO — available from the standard library — is an
instance of the Float_IO package, and it's defined as:
You can use it directly with any object of floating-point type. For example:
with Ada.Float_Text_IO;
procedure Show_Float_Text_IO is
X : constant Float := 2.5;
use Ada.Float_Text_IO;
begin
Put (X);
end Show_Float_Text_IO;
Instantiating generic I/O packages can be useful for derived types. For example,
let's create a new type Price that must be displayed with two decimal
digits after the point, and no exponent.
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Float_IO_Inst is
type Price is digits 3;
package Price_IO is new
Ada.Text_IO.Float_IO (Price);
P : Price;
begin
-- Set to zero => don't display exponent
Price_IO.Default_Exp := 0;
P := 2.5;
Price_IO.Put (P);
New_Line;
P := 5.75;
Price_IO.Put (P);
New_Line;
end Show_Float_IO_Inst;
By adjusting Default_Exp from the Price_IO instance to remove
the exponent, we can control how variables of Price type are displayed.
Just as a side note, we could also have written:
In this case, we're ajusting Default_Aft, too, to get two decimal digits
after the point when calling Put.
In addition to the generic Float_IO package, the following generic
packages are available from Ada.Text_IO:
Enumeration_IO for enumeration types;
Integer_IO for integer types;
Modular_IO for modular types;
Fixed_IO for fixed-point types;
Decimal_IO for decimal types.
In fact, we could rewrite the example above using decimal types:
with Ada.Text_IO; use Ada.Text_IO;
procedure Show_Decimal_IO_Inst is
type Price is delta 10.0 ** (-2) digits 12;
package Price_IO is new
Ada.Text_IO.Decimal_IO (Price);
P : Price;
begin
Price_IO.Default_Exp := 0;
P := 2.5;
Price_IO.Put (P);
New_Line;
P := 5.75;
Price_IO.Put (P);
New_Line;
end Show_Decimal_IO_Inst;
An important application of generics is to model abstract data types
(ADTs). In fact, Ada includes a library with numerous ADTs using
generics: Ada.Containers (described in the containers
section).
A typical example of an ADT is a stack:
generic
Max : Positive;
type T is private;
package Stacks is
type Stack is limited private;
Stack_Underflow, Stack_Overflow : exception;
function Is_Empty (S : Stack) return Boolean;
function Pop (S : in out Stack) return T;
procedure Push (S : in out Stack;
V : T);
private
type Stack_Array is
array (Natural range <>) of T;
Min : constant := 1;
type Stack is record
Container : Stack_Array (Min .. Max);
Top : Natural := Min - 1;
end record;
end Stacks;
package body Stacks is
function Is_Empty (S : Stack) return Boolean is
(S.Top < S.Container'First);
function Is_Full (S : Stack) return Boolean is
(S.Top >= S.Container'Last);
function Pop (S : in out Stack) return T is
begin
if Is_Empty (S) then
raise Stack_Underflow;
else
return X : T do
X := S.Container (S.Top);
S.Top := S.Top - 1;
end return;
end if;
end Pop;
procedure Push (S : in out Stack;
V : T) is
begin
if Is_Full (S) then
raise Stack_Overflow;
else
S.Top := S.Top + 1;
S.Container (S.Top) := V;
end if;
end Push;
end Stacks;
with Ada.Text_IO; use Ada.Text_IO;
with Stacks;
procedure Show_Stack is
package Integer_Stacks is new
Stacks (Max => 10,
T => Integer);
use Integer_Stacks;
Values : Integer_Stacks.Stack;
begin
Push (Values, 10);
Push (Values, 20);
Put_Line ("Last value was "
& Integer'Image (Pop (Values)));
end Show_Stack;
In this example, we first create a generic stack package (Stacks)
and then instantiate it to create a stack of up to 10 integer values.
Let's look at a simple procedure that swaps variables of type
Color:
package Colors is
type Color is (Black, Red, Green,
Blue, White);
procedure Swap_Colors (X, Y : in out Color);
end Colors;
package body Colors is
procedure Swap_Colors (X, Y : in out Color) is
Tmp : constant Color := X;
begin
X := Y;
Y := Tmp;
end Swap_Colors;
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Non_Generic_Swap_Colors is
A, B, C : Color;
begin
A := Blue;
B := White;
C := Red;
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
New_Line;
Put_Line ("Swapping A and C...");
New_Line;
Swap_Colors (A, C);
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
end Test_Non_Generic_Swap_Colors;
In this example, Swap_Colors can only be used for the Color
type. However, this algorithm can theoretically be used for any type,
whether an enumeration type or a complex record type with many
elements. The algorithm itself is the same: it's only the type that
differs. If, for example, we want to swap variables of Integer
type, we don't want to duplicate the implementation. Therefore, such
an algorithm is a perfect candidate for abstraction using generics.
In the example below, we create a generic version of Swap_Colors
and name it Generic_Swap. This generic version can operate on any
type due to the declaration of formal type T.
generic
type T is private;
procedure Generic_Swap (X, Y : in out T);
procedure Generic_Swap (X, Y : in out T) is
Tmp : constant T := X;
begin
X := Y;
Y := Tmp;
end Generic_Swap;
with Generic_Swap;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
procedure Swap_Colors is new
Generic_Swap (T => Color);
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Swap_Colors is
A, B, C : Color;
begin
A := Blue;
B := White;
C := Red;
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
New_Line;
Put_Line ("Swapping A and C...");
New_Line;
Swap_Colors (A, C);
Put_Line ("Value of A is "
& Color'Image (A));
Put_Line ("Value of B is "
& Color'Image (B));
Put_Line ("Value of C is "
& Color'Image (C));
end Test_Swap_Colors;
As we can see in the example, we can create the same Swap_Colors
procedure as we had in the non-generic version of the algorithm by
declaring it as an instance of the generic Generic_Swap procedure. We
specify that the generic T type will be mapped to the Color type
by passing it as an argument to the Generic_Swap instantiation,
The previous example, with an algorithm to swap two values, is one of the
simplest examples of using generics. Next we study an algorithm for
reversing elements of an array. First, let's start with a non-generic
version of the algorithm, one that works specifically for the Color
type:
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It (X : in out Color_Array);
end Colors;
package body Colors is
procedure Reverse_It (X : in out Color_Array) is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : Color;
X_Left : Color
renames X (I);
X_Right : Color
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Reverse_It;
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Non_Generic_Reverse_Colors is
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
for C of My_Colors loop
Put_Line ("My_Color: " & Color'Image (C));
end loop;
New_Line;
Put_Line ("Reversing My_Color...");
New_Line;
Reverse_It (My_Colors);
for C of My_Colors loop
Put_Line ("My_Color: " & Color'Image (C));
end loop;
end Test_Non_Generic_Reverse_Colors;
The procedure Reverse_It takes an array of colors, starts by
swapping the first and last elements of the array, and continues doing that
with successive elements until it reaches the middle of array. At that
point, the entire array has been reversed, as we see from the output of the
test program.
To abstract this procedure, we declare formal types for three components of
the algorithm:
the elements of the array (Color type in the example)
the range used for the array (Integer range in the example)
the actual array type (Color_Array type in the example)
This is a generic version of the algorithm:
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
procedure Generic_Reverse (X : in out Array_T);
procedure Generic_Reverse (X : in out Array_T) is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : T;
X_Left : T
renames X (I);
X_Right : T
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Generic_Reverse;
with Generic_Reverse;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It is new
Generic_Reverse (T => Color,
Index => Integer,
Array_T => Color_Array);
end Colors;
with Ada.Text_IO; use Ada.Text_IO;
with Colors; use Colors;
procedure Test_Reverse_Colors is
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
for C of My_Colors loop
Put_Line ("My_Color: "
& Color'Image (C));
end loop;
New_Line;
Put_Line ("Reversing My_Color...");
New_Line;
Reverse_It (My_Colors);
for C of My_Colors loop
Put_Line ("My_Color: "
& Color'Image (C));
end loop;
end Test_Reverse_Colors;
As mentioned above, we're abstracting three components of the algorithm:
the T type abstracts the elements of the array
the Index type abstracts the range used for the array
the Array_T type abstracts the array type and uses the
formal declarations of the T and Index types.
In the previous example we've focused only on abstracting the reversing
algorithm itself. However, we could have decided to also abstract our small
test application. This could be useful if we, for example, decide to test
other procedures that change elements of an array.
In order to do this, we again have to choose the elements to abstract. We
therefore declare the following formal parameters:
S: the string containing the array name
a function Image that converts an element of type T to a
string
a procedure Test that performs some operation on the array
Note that Image and Test are examples of formal subprograms and
S is an example of a formal object.
Here is a version of the test application making use of the generic
Perform_Test procedure:
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
procedure Generic_Reverse (X : in out Array_T);
procedure Generic_Reverse (X : in out Array_T) is
begin
for I in X'First ..
(X'Last + X'First) / 2 loop
declare
Tmp : T;
X_Left : T
renames X (I);
X_Right : T
renames X (X'Last + X'First - I);
begin
Tmp := X_Left;
X_Left := X_Right;
X_Right := Tmp;
end;
end loop;
end Generic_Reverse;
generic
type T is private;
type Index is range <>;
type Array_T is
array (Index range <>) of T;
S : String;
with function Image (E : T) return String is <>;
with procedure Test (X : in out Array_T);
procedure Perform_Test (X : in out Array_T);
with Ada.Text_IO; use Ada.Text_IO;
procedure Perform_Test (X : in out Array_T) is
begin
for C of X loop
Put_Line (S & ": " & Image (C));
end loop;
New_Line;
Put_Line ("Testing " & S & "...");
New_Line;
Test (X);
for C of X loop
Put_Line (S & ": " & Image (C));
end loop;
end Perform_Test;
with Generic_Reverse;
package Colors is
type Color is (Black, Red, Green,
Blue, White);
type Color_Array is
array (Integer range <>) of Color;
procedure Reverse_It is new
Generic_Reverse (T => Color,
Index => Integer,
Array_T => Color_Array);
end Colors;
with Colors; use Colors;
with Perform_Test;
procedure Test_Reverse_Colors is
procedure Perform_Test_Reverse_It is new
Perform_Test (T => Color,
Index => Integer,
Array_T => Color_Array,
S => "My_Color",
Image => Color'Image,
Test => Reverse_It);
My_Colors : Color_Array (1 .. 5) :=
(Black, Red, Green, Blue, White);
begin
Perform_Test_Reverse_It (My_Colors);
end Test_Reverse_Colors;
In this example, we create the procedure
Perform_Test_Reverse_It as an instance of the generic
procedure (Perform_Test). Note that:
For the formal Image function, we use the 'Image attribute
of the Color type
For the formal Test procedure, we reference the
Reverse_Array procedure from the package.