Anticipated changes, that is, changes that can be reasonably foreseen by the developer of the part, should be provided for as far as possible. Unanticipated change can only be accommodated by carefully structuring a part to be adaptable. Many of the considerations pertaining to maintainability apply. If the code is of high quality, clear, and conforms to well-established design principles such as information hiding, it is easier to adapt in unforeseen ways.
Incoming : Queue;
...
Initialize(Incoming); -- initialization operation
...
if Is_Full(Incoming) then -- query operation
...
end if;
...
Finalize(Incoming); -- finalization operation |
When a reusable part can reasonably be implemented using dynamic data, then any application that must control memory can use the initialization and finalization routines to guard against memory leakage. Then if data structures become dynamic, the applications that are sensitive to these concerns can be easily adapted.
It is also useful to provide reset operations for many objects. To see that a reset and an initiation can be different, consider the analogous situation of a "warm boot" and a "cold boot" on a personal computer.
Even if all of these operations are not appropriate for the abstraction, the exercise of considering them aids in formulating a complete set of operations, others of which may be used by another application.
Some implementations of the language link all subprograms of a package into the executable file, ignoring whether they are used or not, making unused operations a liability (see Guideline 8.4.4). In such cases, where the overhead is significant, create a copy of the fully functional part and comment out the unused operations with an indication that they are redundant in this application.
Language Ref Manual references: 7.2 Package Specifications and Declarations, 12.1 Generic Declarations
If you find yourself writing two very similar routines differing only in the data type they operate on or the subprograms they call, then it is probably better to write the routine once as a generic unit and instantiate it twice to get the two versions you need. When the need arises later to modify the two routines, the change only needs to be made in one place. This greatly facilitates maintenance.
Once you have made such a choice, consider other aspects of the routine that these two instances may have in common but which are not essential to the nature of the routine. Factor these out as generic formal parameters. When the need arises later for a third similar routine, it can be automatically produced by a third instantiation, if you have foreseen all the differences between it and the other two. A parameterized generic unit can be very reusable.
It may seem that the effort involved in writing generic rather than nongeneric units is substantial. However, making units generic is not much more difficult or time-consuming than making them nongeneric once you become familiar with the generic facilities. It is, for the most part, a matter of practice. Also, any effort put into the development of the unit will be recouped when the unit is reused, as it surely will be if it is placed in a reuse library with sufficient visibility. Do not limit your thinking about potential reuse to the application you are working on or to other applications with which you are very familiar. Applications with which you are not familiar or future applications might be able to reuse your software.
After writing a generic unit and placing it in your reuse library, the first thing you are likely to do is to instantiate it once for your particular needs. At this time, it is a good idea to consider whether there are instantiations which are very likely to be widely used. If so, place each such instantiation in your reuse library so that they can be found and shared by others.
Language Ref Manual references: 12.1 Generic Declarations, 12.3 Generic Instantiation
------------------------------------------------------------------------
generic
type Element is limited private;
type Data is array (Positive range <>) of Element;
with function "<" (Left : in Element;
Right : in Element)
return Boolean is <>;
with procedure Swap (Left : in out Element;
Right : in out Element) is <>;
procedure Generic_Sort (Data_To_Sort : in out Data);
------------------------------------------------------------------------ |
The generic body looks just like a regular procedure body and can make full use of the generic formal parameters in implementing the sort algorithm:
------------------------------------------------------------------------
procedure Generic_Sort (Data_To_Sort : in out Data) is
begin
...
for I in Data_To_Sort'Range loop
...
...
if Data_To_Sort(J) < Data_To_Sort(I) then
Swap(Data_To_Sort(I), Data_To_Sort(J));
end if;
...
...
end loop;
...
end Generic_Sort;
------------------------------------------------------------------------ |
The generic procedure can be instantiated as:
type Integer_Array is array (Positive range <>) of Integer;
procedure Swap (Left : in out Integer;
Right : in out Integer);
procedure Sort is
new Generic_Sort (Element => Integer,
Data => Integer_Array); |
or
subtype String_80 is String (1 .. 80);
type String_Array is array (Positive range <>) of String_80;
procedure Swap (Left : in out String_80;
Right : in out String_80);
procedure Sort is
new Generic_Sort (Element => String_80,
Data => String_Array); |
and called as:
Integer_Array_1 : Integer_Array (1 .. 100); ... Sort(Integer_Array_1); |
or
String_Array_1 : String_Array (1 .. 100); ... Sort(String_Array_1); |
Element data type as a generic limited
private type parameter so that it assumes as little as possible about the data
type of the objects actually being operated on. It also takes Data as a
generic formal parameter so that instantiations can have entire arrays passed
to them for sorting. Finally, it explicitly requires the two operators that it
needs to do the sort: comparison and swap.The sort algorithm is encapsulated
without reference to any data type. The generic can be instantiated to sort an
array of any data type.Language Ref Manual references: 12.1 Generic Declarations, 12.1.2 Generic Formal Types
------------------------------------------------------------------------ package Bounded_Stack is subtype Element is Integer; Maximum_Stack_Size : constant := 100; procedure Push (New_Element : in Element); procedure Pop (Top_Element : out Element); Overflow : exception; Underflow : exception; ... end Bounded_Stack; ------------------------------------------------------------------------ |
The second is an abstract data type (ADT). It differs from the ADO by
exporting the Stacks type, which allows the user to declare any number of
stacks of integers. Note that since multiple stacks may now exist, it is
necessary to specify a Stack argument on calls to Push and Pop.
------------------------------------------------------------------------
package Bounded_Stack is
subtype Element is Integer;
type Stack is limited private;
Maximum_Stack_Size : constant := 100;
procedure Push (On_Top : in out Stack;
New_Element : in Element);
procedure Pop (From_Top : in out Stack;
Top_Element : out Element);
Overflow : exception;
Underflow : exception;
...
private
type Stack_Information;
type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------ |
The third is a parameterless generic abstract data object (GADO). It differs from the ADO (the first example) simply by being generic, so that the user can instantiate it multiple times to obtain multiple stacks of integers.
------------------------------------------------------------------------ generic package Bounded_Stack is subtype Element is Integer; Maximum_Stack_Size : constant := 100; procedure Push (New_Element : in Element); procedure Pop (Top_Element : out Element); Overflow : exception; Underflow : exception; ... end Bounded_Stack; ------------------------------------------------------------------------ |
The fourth is a slight variant on the third, still a generic abstract data
object (GADO) but with parameters. It differs from the third example by making
the data type of the stack a generic parameter so that stacks of data types
other than Integer can be created.
Also, Max_Stack_Size has been made a
generic parameter which defaults to 100 but can be specified by the user,
rather than a constant defined by the package.
------------------------------------------------------------------------
generic
type Element is limited private;
with procedure Assign (From : in Element;
To : in out Element);
Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
procedure Push (New_Element : in Element);
procedure Pop (Top_Element : in out Element);
Overflow : exception;
Underflow : exception;
...
end Bounded_Stack;
------------------------------------------------------------------------ |
Finally, the fifth is a generic abstract data type (GADT). It differs from the
GADO in the fourth example in the same way that the ADT in the second example
differed from the ADO in the first example; it exports the Stacks type, which
allows the user to declare any number of stacks.
------------------------------------------------------------------------
generic
type Element is limited private;
with procedure Assign (From : in Element;
To : in out Element);
Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
type Stack is limited private;
procedure Push (On_Top : in out Stack;
New_Element : in Element);
procedure Pop (From_Top : in out Stack;
Top_Element : in out Element);
Overflow : exception;
Underflow : exception;
...
private
type Stack_Information;
type Stack is access Stack_Information;
end Bounded_Stack;
------------------------------------------------------------------------ |
Push and Pop). For these reasons, an ADT or GADT is almost always
preferable to an ADO or GADO, respectively.A GADO is similar to an ADT in one way: it allows multiple objects to be created by the user. With an ADT, multiple objects can be declared using the type defined by the ADT package. With a GADO (even a GADO with no generic formal parameters, as shown in the third example), the package can be instantiated multiple times to produce multiple objects. However, the similarity ends there. The multiple objects produced by the instantiations suffer from all restrictions described above for ADOs; they cannot be used in arrays or records or passed as parameters. Furthermore, the objects are each of a different type, and no operations are defined to operate on more than one of them at a time. For example, there cannot be an operation to compare two such objects or to assign one to another. The multiple objects declared using the type defined by an ADT package suffer from no such restrictions; they can be used in arrays and records and can be passed as parameters. Also, they are all declared to be of the same type, so that it is possible for the ADT package to provide operations to assign, compare, copy, etc. For these reasons, an ADT is almost always preferable to a parameterless GADO.
The biggest advantage of a GADT or GADO over an ADT or ADO, respectively, is that the GADT and GADO are generic and can thus be parameterized with types, subprograms, and other configuration information. Thus, as shown above, a single generic package can support bounded stacks of any data type and any stack size, while the ADT and ADO above are restricted to stacks of Integer, no more than 100 in size. For this reason, a GADO or GADT is almost always preferable to an ADO or ADT.
The list of examples above is given in order of increasing power and flexibility, starting with an ADO and ending with a GADT. These advantages are not expensive in terms of complexity or development time. The specification of the GADT above is not significantly harder to write or understand than the specification of the ADO. The bodies are also nearly identical. Compare the body for the simplest version, the ADO:
------------------------------------------------------------------------
package body Bounded_Stack is
type Stack_Slots is array (Natural range <>) of Element;
type Stack_Information is
record
Slots : Stack_Slots (1 .. Maximum_Stack_Size);
Index : Natural := 0;
end record;
Stack : Stack_Information;
---------------------------------------------------------------------
procedure Push (New_Element : in Element) is
begin
if Stack.Index >= Maximum_Stack_Size then
raise Overflow;
end if;
Stack.Index := Stack.Index + 1;
Stack.Slots(Stack.Index) := New_Element;
end Push;
---------------------------------------------------------------------
procedure Pop (Top_Element : out Element) is
begin
if Stack.Index <= 0 then
raise Underflow;
end if;
Top_Element := Stack.Slots(Stack.Index);
Stack.Index := Stack.Index - 1;
end Pop;
---------------------------------------------------------------------
...
end Bounded_Stack;
------------------------------------------------------------------------ |
with the body for the most powerful and flexible version, the GADT:
------------------------------------------------------------------------
package body Bounded_Stack is
type Stack_Slots is array (Natural range <>) of Element;
type Stack_Information is
record
Slots : Stack_Slots (1 .. Maximum_Stack_Size);
Index : Natural := 0;
end record;
---------------------------------------------------------------------
procedure Push (On_Top : in out Stack;
New_Element : in Element) is
begin
if On_Top.Index >= Maximum_Stack_Size then
raise Overflow;
end if;
On_Top.Index := On_Top.Index + 1;
Assign(From => New_Element,
To => On_Top.Slots(On_Top.Index));
end Push;
---------------------------------------------------------------------
procedure Pop (From_Top : in out Stack;
Top_Element : in out Element) is
begin
if From_Top.Index <= 0 then
raise Underflow;
end if;
Assign(From => From_Top.Slots(From_Top.Index),
To => Top_Element);
From_Top.Index := From_Top.Index - 1;
end Pop;
---------------------------------------------------------------------
...
end Bounded_Stack;
------------------------------------------------------------------------ |
There are only two differences. First, the ADO declares a local object called
Stack, while the GADT has one additional parameter (called Stack) on each of
the exported procedures Push and Pop. Second, the GADT uses the Assign
procedure rather than the assignment operator ":=" because the generic formal
type Element was declared limited private. This second difference could have
been avoided by declaring Element as private, but this is not recommended
because it reduces the composability of the generic reusable part.
Language Ref Manual references: 3.6 Array Types, 7.4.4 Limited Types, 12.1 Generic Declarations, 12.1.2 Generic Formal Types, 12.3.2 Matching Rules for Formal Private Types
------------------------------------------------------------------------
generic
type Element is limited private;
...
package Unbounded_List is
type List is limited private;
procedure Insert (New_Element : in Element;
Into : in out List);
-- Passive (generic) iterator.
generic
with procedure Process (Each : in out Element);
procedure Iterate (Over : in List);
-- Active iterator
type Iterator is limited private;
procedure Initialize (Index : in out Iterator;
Existing_List : in List);
function More (Index : in Iterator)
return Boolean;
procedure Advance (Index : in out Iterator);
function Current (Index : in Iterator)
return Element;
procedure Finalize (Index : in out Iterator);
...
private
...
end Unbounded_List;
------------------------------------------------------------------------ |
After instantiating the generic package, and declaring a list, as:
------------------------------------------------------------------------
with Unbounded_List;
procedure List_User is
type Employee is ...;
package Roster is
new Unbounded_List (Element => Employee, ...);
Employee_List : Roster.List; |
the passive iterator is instantiated, specifying the name of the routine which should be called for each list element when the iterator is called.
---------------------------------------------------------------------
procedure Process_Employee (Each : in out Employee) is
begin
...
-- Perform the required action for EMPLOYEE here.
end Process_Employee;
---------------------------------------------------------------------
procedure Process_All is
new Roster.Iterate (Process => Process_Employee); |
The passive iterator can then be called, as:
begin -- List_User Process_All(Employee_List); end List_User; ------------------------------------------------------------------------ |
Alternatively, the active iterator can be used, without the second instantiation required by the passive iterator, as:
Iterator : Roster.Iterator;
procedure Process_Employee (Each : in Employee) is separate;
begin -- List_User
Roster.Initialize (Index => Iterator,
Existing_List => Employee_List);
while Roster.More(Iterator) loop
Process_Employee(Each => Roster.Current(Iterator));
Roster.Advance(Iterator);
end loop;
Roster.Finalize(Iterator);
end List_User;
------------------------------------------------------------------------ |
Active and passive iterators each have their advantages, but neither is appropriate in all situations. Therefore, it is recommended that both be provided to give the user a choice of which to use in each situation.
Passive iterators are simpler and less error-prone than active iterators, in
the same way that the for loop is simpler and less error-prone than the while
loop. There are fewer mistakes that the user can make in using a passive
iterator. Simply instantiate it with the routine to be executed for each list
element, and call the instantiation for the desired list. Active iterators
require more care by the user. The iterator must be declared, then initialized
with the desired list, then Current and Advance must be called in a loop until
More returns false, then the iterator must be terminated. Care must be taken
to perform these steps in the proper sequence. Care must also be taken to
associate the proper iterator variable with the proper list variable. It is
possible for a change made to the software during maintenance to introduce an
error, perhaps an infinite loop.
On the other hand, active iterators are more flexible than passive iterators.
With a passive iterator, it is difficult to perform multiple, concurrent,
synchronized iterations. For example, it is much easier to use active
iterators to iterate over two sorted lists, merging them into a third sorted
list. Also, for multidimensional data structures, a small number of active
iterator routines may be able to replace a large number of passive iterators,
each of which implements one combination of the active iterators. Consider,
for example, a binary tree. In what order should the passive iterator visit
the nodes? Depth first? Breadth first? What about the need to do a binary
search of the tree? Each of these could be implemented as a passive iterator,
but it may make more sense to simply define the More_Left, More_Right,
Advance_Left, and Advance_Right routines required by the active iterator to
cover all combinations.Finally, active iterators can be passed as generic
formal parameters while passive iterators cannot because passive iterators are
themselves generic, and generic units cannot be passed as parameters to other
generic units.
For either type of iterator, semantic questions can arise about what happens when the data structure is modified as it is being iterated. When writing an iterator, be sure to consider this possibility, and indicate with comments the behavior which occurs in such a case. It is not always obvious to the user what to expect. For example, to determine the "closure" of a mathematical "set" with respect to some operation, a common algorithm is to iterate over the members of the set, generating new elements and adding them to the set. In such a case, it is important that elements added to the set during the iteration be encountered subsequently during the iteration. On the other hand, for other algorithms it may be important that the set which it iterated is the set as it existed at the beginning of the iteration. In the case of a prioritized list data structure, if the list is iterated in priority order, it may be important that elements inserted at lower priority than the current element during iteration not be encountered subsequently during the iteration, but that elements inserted at a higher priority should be encountered. In any case, make a conscious decision about how the iterator should operate, and document that behavior in the package specification.
Deletions from the data structure also pose a problem for iterators. It is a
common mistake for a user to iterate over a data structure, deleting it piece
by piece during the iteration. If the iterator is not prepared for such a
situation, it is possible to end up dereferencing a null pointer or committing
a similar error. Such situations can be prevented by storing extra information
with each data structure which indicates whether it is currently being
iterated, and using this information to disallow any modifications to the data
structure during iteration. When the data structure is declared as a limited private
type, as should usually be the case when iterators are involved, the
only operations defined on the type are declared explicitly in the package
which declares the type, making it possible to add such tests to all
modification operations.
Language Ref Manual references: 2.7 Comments, 12.1 Generic Declarations
in out rather than out for parameters of a generic formal
subprogram, when the parameters are of an imported limited type.
------------------------------------------------------------------------
generic
type Item is private;
type Key is private;
with function Key_Of (Current : in Item) return Key;
package List_Manager is
type List is limited private;
procedure Insert (Into : in List;
New_Item : in Item);
procedure Retrieve (From : in List;
Using : in Key;
Match : in out Item);
private
type List is ...
end List_Manager;
------------------------------------------------------------------------ |
The second example is improved by using limited private generic formal types
and importing the assignment operation for Item and the equality operator for
Key.
------------------------------------------------------------------------
generic
type Item is limited private;
type Key is limited private;
with procedure Assign (From : in Item;
To : in out Item);
with function "=" (Left : in Key;
Right : in Key)
return Boolean;
with function Key_Of (Current : in Item)
return Key;
package List_Manager is
type List is limited private;
procedure Insert (Into : in List;
New_Item : in Item);
procedure Retrieve (From : in List;
Using : in Key;
Match : in out Item);
private
type List is ...
end List_Manager;
------------------------------------------------------------------------ |
Therefore, generic formal types should almost always be limited private rather
than just private. This restricts the operations available on the imported
type within the generic unit body but provides maximum flexibility for the
user of the generic unit. Any operations required by the generic body should
be explicitly imported as generic formal subprograms. In the second example
above, only the operations required for managing a list of items with keys are
imported: Assign provides the ability to store items in the list, and Key_Of
and "=" support determination and comparison of keys during retrieval
operations. No other operations are required to manage the list. Specifically,
there is no need to be able to assign keys or compare entire items for
equality. Those operations would have been implicitly available if a private
type had been used for the generic formal type, and any actual type for which
they were not defined could not have been used with this generic unit.
The situation is reversed for types exported by a reusable part. For exported types, the restrictions specified by limited and limited private are restrictions on the user of the part, not on the part itself. To provide maximum capability to the user of a reusable part, export types with as few restrictions as possible. Apply restrictions as necessary to protect the integrity of the exported data structures and the abstraction for the various implementations envisioned for that generic.
In the example above, the List type is exported as limited private to hide the
details of the list implementation and protect the structure of a list.
Limited private is chosen over private to prevent the user from being able to
use the predefined assignment operation. This is important if the list is
implemented as an access type pointing to a linked lists of records, because
the predefined assignment would make copies of the pointer, not copies of the
entire list, which the user may not realize. If it is expected that the user
needs the ability to copy lists, then a copy operation should be explicitly
exported.
Because they are so restrictive, limited private types are not always the best choice for types exported by a reusable part. In a case where it makes sense to allow the user to make copies of and compare data objects, and when the underlying data type does not involve access types (so that the entire data structure gets copied or compared), then it is better to export a (nonlimited) private type. In cases where it does not detract from the abstraction to reveal even more about the type, then a nonprivate type (e.g., a numeric, enumerated, record, or array type) should be used.
For cases where limited private types are exported, the package should explicitly provide equality and assignment operations, if appropriate to the abstraction. Limited private is almost always appropriate for types implemented as access types. In such cases, predefined equality is seldom the most desirable semantics. In such cases, also consider providing both forms of assignment (assignment of a reference and assignment of a copy).
When the parameters are of an imported limited type, using mode in out instead
of out for parameters of a generic formal subprogram is important for the
following reason. Ada allows an out mode parameter of a limited private type
on a subprogram only when the subprogram is declared in the visible part of
the package that declares the private type. See
Section 7.4.4(4) of the
Ada
Language Reference Manual (Department of Defense 1983).There is no such
restriction in parameters of mode in out.The result of this is that if you
define a generic with a limited generic formal type and a generic formal
subprogram with an out parameter of that type, then the generic can only be
instantiated with a limited private actual type if the package which declares
that type also declares a subprogram with exactly the same profile (number and
types or arguments and return value) as your generic formal subprogram. A
potential user who wants to instantiate your generic with a limited type
defined in another package will not be able to write a subprogram to pass as
the generic actual.
It should also be noted that the predefined packages, Sequential_IO and
Direct_IO, take private types. This will complicate IO requirements for
limited private types and should be considered during design.
Language Ref Manual references: 12.1.2 Generic Formal Types, 12.1.3 Generic Formal Subprograms, 12.3 Generic Instantiation