Generics¶
Formal packages¶
Abstracting definitions into packages¶
In this section and in the next ones, we will reuse the generic reversing algorithm that we discussed in the chapter about generics from the introductory course (Generics):
In that example, we were declaring three formal types for the
Generic_Reverse_Array
procedure: a type T
, a range Index
and the array type Array_T
. However, we could abstract the array
definition into a separate package and reuse it for the generic procedure.
This could be potentially useful in case we want to create more generic
procedures for the same array.
In order to achieve this, we start by first specifying a generic package that contains the generic array type definition:
As you can see, this definition is the same that we've seen in the
previous section: we just moved it into a separate package. Now, we have a
definition of Array_T
that can be reused in multiple places.
The next step is to reuse the Simple_Generic_Array_Pkg
package in
the Generic_Reverse_Array
procedure. By doing this, we can
eliminate the declaration of the Index
and Array_T
types
that we had before, since the definition will come from the
Simple_Generic_Array_Pkg
package.
In order to reuse the Simple_Generic_Array_Pkg
package in the
Generic_Reverse_Array
procedure, we need to use a formal package
parameter in the form:
with package P is new Simple_Generic_Array_Pkg(<params>)
This will allow us to reuse definitions from the generic package.
This is the updated version of the our test application for the reversing algorithm:
In this example, we're first instantiating the
Simple_Generic_Array_Pkg
package, thereby creating the
Color_Pkg
package. We then proceed to use this Color_Pkg
package in the instantiation of the generic Reverse_Array
procedure. Also, in the declaration of the My_Colors
array, we make
use of the array type definition from the Color_Pkg
package.
Formal package parametrization¶
Note that we're using partial parametrization for the formal package
parameter P
in the previous example. Partial parametrization makes
use of others => <>
to indicate that the generic declaration takes
the definitions from the package argument provided in the generic
instantiation:
For the previous example, the definitions come from the declarations of
the Color_Pkg
package:
A complete parametrization, in constrast, contains the definition of all types in the generic declaration. For example:
Another approach is to take all definitions from the formal package parameter:
In this case, package P
contains all type and subprogram
definitions that are used by the generic Reverse_Array
procedure.
By using the box syntax (<>)
, we indicate that we make use of all
definitions from the formal package parameter.
This kind of formal package parameter containing definitions is called a signature package. Usually, a signature package is a generic package and doesn't have a package body. Also, it isn't useful as a standalone package. Instead, it's used to group types and subprogram declarations that will be used as a formal package parameter. This approach is useful for creating separate specifications for types and subprograms that don't belong together. Also, multiple signature packages can be cascaded to create more complex generic implementations.
Abstracting procedures into packages¶
In the previous example, we moved the array type definition into a
separate package, but left the generic procedure (Reverse_Array
) in
the test application. We could also move the generic procedure into the
generic package:
The advantage of this approach is that we don't need to repeat the formal
declaration for the Reverse_Array
procedure. Also, this simplifies
the instantiation in the test application.
However, the disadvantage of this approach is that it also increases code size: every instantiation of the generic package generates code for each subprogram from the package. Also, compilation time tends to increase significantly. Therefore, developers must be careful when considering this approach.
Because we have a procedure declaration in the generic package, we need a
corresponding package body. Here, we can simply reuse the existing code
and move the procedure into the package body. In the test application, we
just instantiate the Generic_Array_Pkg
package and make use of the
array type (Array_T
) and the procedure (Reverse_Array
):
Color_Pkg.Reverse_Array (My_Colors);
This is the generic package body:
Abstracting the test application¶
In the previous examples, we've focused only on abstracting the reversing algorithm. However, we could have decided to also abstract our little test application. This could be useful if we, for example, decide to test other procedures that change elements of an array.
In order to achieve this, we have to abstract quite a few elements. We will therefore declare the following formal parameters:
- the string
S
containing the array name;- the formal
Generic_Array_Pkg
package parameter, which is a signature package implemented in the previous section;- the formal
Image
function that converts an element of typeT
to a string;- the formal
Pkg_Test
procedure that performs some operation on the array.
Note that Image
and Pkg_Test
are examples of formal
subprograms, which have been discussed in the introductory course. Also,
note that S
is an example of a formal object, which we discuss in
later section.
This is a version of the test application that makes use of the generic
Perform_Test
procedure:
In this example, we create the procedure
Perform_Test_Reverse_Color_Array
as an instance of the generic
procedure (Perform_Test
). Note that:
- For the formal
Image
function, we make use of the'Image
attribute of theColor
type- For the formal
Pkg_Test
procedure, we reference theReverse_Array
procedure from the package.
Note that this example includes a formal package declaration:
with package Array_Pkg is new Generic_Array_Pkg (<>);
Previously, we've seen package instantiations that define the elements. For example:
package Color_Pkg is new Generic_Array_Pkg (T => Color, Index => Integer);
In this case, however, we're simply using (<>)
, as discussed in the
section on
formal package parametrization.
This means that Perform_Test
makes use of the default definition
used for the instance of Generic_Array_Pkg
.
Cascading signature packages¶
In the code example from the previous section, we declared four formal
parameters for the Perform_Test
procedure. Two of them are directly
related to the array that we're using for the test:
S
: the string containing the array name- the function
Image
that converts an elements of the array to a string
We could abstract our implementation even further by moving these elements
into a separate package named Generic_Array_Bundle
and reference
the Generic_Array_Pkg
there. This would create a chain of signature
packages:
Generic_Array_Bundle <= Generic_Array_Pkg
This strategy demonstrates that, in Ada, it is really straightforward to make use of generics in order to abstracts algorithms.
First, let us define the new Generic_Array_Bundle
package, which
references the Generic_Array_Pkg
package and the two formal elements
(S
and Image
) mentioned previously:
Then, we update the definition of Perform_Test
:
Note that, in this case, we reduce the number of formal parameters to only two:
Array_Bundle
: an instance of the newGeneric_Array_Bundle
package
- the procedure
Pkg_Test
that we already had before
We could go even further and move Perform_Test
into a separate
package. However, this will be left as an exercise for the reader.
Formal objects¶
Formal objects are used to bind objects to a generic specification. They
are similar to parameters in subprograms and can have in
or
in out
modes.
One of the simplest applications of formal objects is to use them to configure a generic subprogram or package during instantiation. For example, we can implement a generic function that processes an array of floating-point values and calculates an output value. This calculation is implemented in two versions:
- a standard version;
- a faster version that is less accurate than the standard version.
While the generic implementation offers both variants, developers can select the version that is more appropriate for their system during instantiation.
In this example, we instantiate the fast version of Gen_Calc
.
Input-output formal objects¶
Formal objects with in out
mode are used to bind objects in an
instance of a generic specification. For example, we may bind a global
object from a package to the instantiation of a generic procedure, so that
all calls to this instance make use of that object internally.
In the application below, we create a database using a container and bind it to procedures that display information from the database in a specific format.
The Data_Elements
package describes the data fields of the data
container. It also includes an Image
function that returns a string
based on the specified field.
This is the corresponding package body:
Note that the age field in the Image
function (represented by
Age_F
) isn't a field from the data container, but a calculated
value instead.
The Data
package below implements the data container using a
vector. It includes the generic procedure Display
that exhibits the
information from the data container based on the fields specified by the
developer at the procedure instantiation.
Note that, in addition to Container
, which is a formal input-output
object, we make use of the Fields
and Header
objects, which
are formal input objects. Also, note that we could have declared
Container
as a parameter of Display
instead of declaring it
as a formal object:
generic
Fields : Data_Fields_Array;
Header : String := "";
procedure Display (Container : in out Data_Container);
In this case, we wouldn't be able to bind a local Container
object
to the instantiation of the Display
procedure. Instead, we would
always have to pass the container as an argument. Potentially, we could
pass the wrong container to the procedure. By using a formal input-output
object, we make sure that a specific object is bound to the procedure.
This design decision ensures that we always have the same object being
used in all calls to an instance of the Display
procedure.
This is the corresponding body of the Data
package:
Finally, we implement the Test_Data_Container
procedure, which
makes use of the data container:
In this example, we declare the data container C
and bind it to
two instantiations of the Display
procedure:
Display_First_Name_Age
, which displays the first name and age of each person from the database;Display_Name_Birthday
, which displays the full name and birthday of each person.
Generic interfaces¶
Generating subprogram specifications¶
Generic interfaces can be used to generate a collection of pre-defined
subprograms for new types. For example, let's suppose that, for a given
type T
, we need at least a pair of subprograms that set and get
elements of type T
based on another type. We might want to convert
back and forth between the types T
and Integer
. In addition,
we might want to convert from and to other types (e.g., Float
). To
implement this, we can define the following generic interface:
In this example, the package Set_Get
defines subprograms that allow
converting from any definite type (TD
) and the interface type
(TI
).
We then proceed to declare packages for converting between Integer
and Float
types and the interface type. Also, we declare an actual
tagged type that combines these conversion subprograms into a single type:
First, we declare the packages Set_Get_Integer
and
Set_Get_Float
based on the generic Set_Get
package. Next,
we declare My_Type
based on the interface type from these two
packages. By doing this, My_Type
now needs to implement the actual
conversion from and to Integer
and Float
types.
Note that, in the private part of My_Type
, we're storing the
floating-point and integer representations that we receive in the calls to
the Set
procedures. However, we could have complex data as well and
just use conversion subprograms to provide a simplified representation of
the complex data.
This is just an example on how we could implement these Set
and
Get
subprograms:
As expected, declaring and using variable of My_Type
is
straightforward:
Facilitating arrays of interfaces¶
Formal interfaces can facilitate the handling of arrays of interface
types. Let's consider an interface type TI
and the derived tagged
types T
and T2
. We may declare arrays containing elements
that access the TI
class. These arrays can be initialized with
elements that access types T
or T2
. Also, we may process
these arrays with an operation Op
using the API of the TI
interface.
This is a test application that declares an array A
of the
interface type TI
and calls Op
for A
:
This example doesn't work if we use an array of the derived type T
:
with TI_Pkg; use TI_Pkg;
with T_Pkg; use T_Pkg;
procedure Test_T is
A : T_Array (1 .. 3) :=
(1 => new T,
2 => new T2,
3 => new T);
begin
Op (A);
end Test_T;
This is incorrect because Op
expects an array of type TI
,
not T
. Even if the type T
is derived from TI
, the
corresponding array type is not. Formal interfaces can be used to create
a generic version of Op
that operates directly on an array of
type T
. Let's look at an example.
The example below calculates the average of interface types that are
convertible to floating-point values. We consider that a type is
convertible to floating-point if it provides a To_Float
function.
This is implemented with the Float_Cnvt_Type
interface. We also
declare a generic package containing the Average
function, which
calculates the average of an array containing elements of a
convertible type (i.e. any type derived from the Float_Cnvt_Type
interface).
This is the corresponding package body containing the implementation of
the generic Average
function:
In the App_Data
package, we declare two types derived from
Float_Cnvt_Type
: T
and T2
. We also declare the
corresponding To_Float
functions.
This is the corresponding package body:
Finally, this is a test application that declares an array of
convertible types and calls the Average
function to calculate
the average of all elements.
In this example, we declare the array A
with elements of both
T
and T2
types. After initializing the elements of A
,
we call the Average
function from Ops
, an instance of the
generic package Float_Interface_Pkg.Ops
.
Discussion: Generic interfaces vs. other approaches¶
Generic synchronized interfaces¶
Generic synchronized interfaces are a specialized case of generic interfaces that can be used for task types and protected types. Since generic synchronized interfaces are similar to generic interfaces, we can reuse the previous source-code example with minimal adaptations.
When adapting the Gen_Interface
package, we just need to make use
of the synchronized
keyword:
Note that we're also renaming some packages (e.g., renaming
Gen_Interface
to Gen_Sync_Interface
) to better differentiate
between them. This approach is used in the adaptations below as well.
When adapting the My_Type_Pkg
, we again need to make use of
the synchronized
keyword. Also, we need to declare My_Type
as a protected type and adapt the subprogram and component declarations.
Note that we could have used a task type instead. This is the adapted
package:
In the package body, we just need to adapt the access to components in the subprograms:
Finally, the main application doesn't require adaptations:
Generic numeric types¶
Ada supports the use of numeric types for generics. This can be used to describe a numeric algorithm independently of the actual data type. We'll see examples below.
This is the corresponding syntax:
- For floating-point types:
type T is digits <>;
- For binary fixed-point type:
type T is delta <>;
- For decimal fixed-point types:
type T is delta <> digits <>;
In this section, we discuss generic floating-point and binary fixed-point types.
Generic floating-point types¶
Simple generic package¶
Let's look at an example of a generic package containing a procedure that
saturates floating-point numbers. In this code, we work with a
normalized range between -1.0 and 1.0. Due to the fact that some
calculations might lead to results outside this range, we use the
Saturate
procedure to put values back into the normalized range.
This is the package specification:
This is the package body:
Finally, we create a test application:
In this application, we create two instances of the Gen_Float_Ops
package: one for the Float
type and one for the Long_Float
type. We then make use of computations whose results are outside the
normalized range. By calling the Saturate
procedure, we ensure that
the values are inside the range again.
Operations in generic packages¶
In this section, we discuss how to declare operations associated with floating-point types in generic packages.
Let's first define a package that implements a new type My_Float
based on the standard Float
type. For this type, we override the
addition operator with an implementation that saturates the value after
the actual addition.
This is the package specification:
This is the corresponding package body:
Next, we create a package containing a procedure that accumulates floating-point values. This is the package specification:
In this specification, we declare a formal function for the addition
operator using with function
. This operator is used by the
Acc
procedure in the package body. Also, because we use <>
in the specification, the corresponding addition operator for type
F
is selected.
This is the package body:
This is a test application that makes use of the Float_Types
and
Gen_Float_Acc
packages.
We create an instance of the Gen_Float_Acc
by using the
My_Float
type declared in the Float_Types
package. Because
we used <>
in the specification of function "+"
(in the
Gen_Float_Acc
package), the compiler will automatically select
the addition operator that we've overriden in the Float_Types
package, so that we don't need to specify it in the package instantiation.
The main reason for the formal subprogram in the specification of the
Gen_Float_Acc
package is that it prevents the compiler from
selecting the standard operator. We could have removed the
function "+"
from the specification, as illustrated in the
example below, where we modified the Gen_Float_Acc
package:
generic
type F is digits <>;
-- no "with function" here!
package Gen_Float_Acc is
procedure Acc (V : in out F; S : F);
end Gen_Float_Acc;
package body Gen_Float_Acc is
procedure Acc (V : in out F; S : F) is
begin
-- Using standard addition for universal floating-point
-- type (digits <>) here:
V := V + S;
end Acc;
end Gen_Float_Acc;
In this case, however, even though we declared a custom addition operator
for the My_Float
type in the Float_Types
package, an
instantiation of the modified Gen_Float_Acc
package would always
make use of the standard addition:
-- This makes use of the type definition of My_Float, but not its
-- overriden operators.
package Float_Ops is new Gen_Float_Acc (F => My_Float);
Because the type F
is declared as digits <>
, which
corresponds to the universal floating-point data type, the compiler
selects operators associated with the universal floating-point data type
in the package body. By specifying the formal subprogram, we make sure
that the operator associated with the actual type is used.
Alternatively, we could make use of the Float_Types
package
directly in the generic package. For example:
In this case, because the formal type is now based on My_Float
, the
corresponding operator for My_Float
is used in the Acc
procedure.
Generic fixed-point types¶
Simple generic package¶
In the previous section, we looked into an example of saturation for generic floating-point types. Let's adapt this example for fixed-point types. This is the package specification:
For the fixed-point version, we specify the normalized range in the
definition of the data type. Therefore, any computation that leads to
values out of the normalized range will raise a Constraint_Error
exception. In order to circumvent this, we can declare a fixed-point data
type with a wider range and use it in combination with the actual
operation that we want to perform -- an addition, in this case. This
approach can be seen in the implementation of Sat_Add
, which
computes the addition using the local Ovhd_Fixed
type with wider
range, calls the Saturate
procedure and converts the data type back
into the original range.
Ovhd_Fixed
is a 64-bit fixed-point data type. By using
Assert`s in the package body that compare this data type to the
formal :ada:`F
type from the package specification, we ensure that the
local fixed-point data type has enough overhead to cope with any
fixed-point operation that we want to implement. Also, we ensure that we
don't lose precision when converting back-and-forth between the local type
and the original type.
We then use the Gen_Fixed_Ops
package in a test application:
In this test application, we declare two fixed-point data types:
the 16-bit type Fixed
and the 32-bit type Long_Fixed
.
These types are used to create instances of the Gen_Fixed_Ops
. By
calling Sat_Add
, we ensure that the result of adding fixed-point
values will always be in the allowed range and the computation will never
raise an exception.
Operations in generic packages¶
In this section, we discuss how to declare operations associated with fixed-point types in generic packages. We start by adapting the examples used for floating-point in the previous section, so that fixed-point types are used instead.
First, we define a package that implements a new fixed-point type called
Fixed
. For this type, we override the addition operator with an
implementation that saturates the value after the actual addition. This is
the package specification:
In the package body, we make use of the Gen_Fixed_Ops
package that
we discussed earlier in the previous section. By instantiating the
Gen_Fixed_Ops
package, we can use the Sat_Add
function in
the implementation of the saturating addition operator.
Next, we create a package containing a procedure that accumulates fixed-point values. This is the package specification:
In this specification, we declare a formal function for the addition
operator using with function
. This operator is used by the
Acc
procedure in the package body, which we show next.
This is a test application that makes use of the Fixed_Types
and
Gen_Fixed_Acc
packages.
We create an instance of the Gen_Fixed_Acc
by using the
Fixed
type declared in the Fixed_Types
package. We then
call Acc
to accumulate and saturate a fixed-point variable.
As mentioned earlier in the section on generic floating-point types, the
main reason for the formal subprogram in the specification of the
Gen_Fixed_Acc
package is that it prevents the compiler from
selecting the standard operator. Alternatively, we could make use of the
Fixed_Types
package directly in the generic package:
with Fixed_Types; use Fixed_Types;
generic
type F is new Fixed;
package Gen_Fixed_Acc is
procedure Acc (V : in out F; S : F);
end Gen_Fixed_Acc;