Ada 95 Quality and Style Guide                      Chapter 8

CHAPTER 8
Reusability

CHAPTER 8 Reusability
8.1 UNDERSTANDING AND CLARITY
8.2 ROBUSTNESS
8.3 ADAPTABILITY
8.4 INDEPENDENCE
8.5 SUMMARY

Reusability is the extent to which code can be used in different applications with minimal change. As code is reused in a new application, that new application partially inherits the attributes of that code. If the code is maintainable, the application is more maintainable. If it is portable, then the application is more portable. So this chapter's guidelines are most useful when all of the other guidelines in this book are also applied.

Several guidelines are directed at the issue of maintainability. Maintainable code is easy to change to meet new or changing requirements. Maintainability plays a special role in reuse. When attempts are made to reuse code, it is often necessary to change it to suit the new application. If the code cannot be changed easily, it is less likely to be reused.

There are many issues involved in software reuse: whether to reuse parts, how to store and retrieve reusable parts in a library, how to certify parts, how to maximize the economic value of reuse, how to provide incentives to engineers and entire companies to reuse parts rather than reinvent them, and so on. This chapter ignores these managerial, economic, and logistic issues to focus on the single technical issue of how to write software parts in Ada to increase reuse potential. The other issues are just as important but are outside of the scope of this book.

One of the design goals of Ada was to facilitate the creation and use of reusable parts to improve productivity. To this end, Ada provides features to develop reusable parts and to adapt them once they are available. Packages, visibility control, and separate compilation support modularity and information hiding (see guidelines in
Sections 4.1, 4.2, 5.3, and 5.7). This allows the separation of application-specific parts of the code, maximizes the general purpose parts suitable for reuse, and allows the isolation of design decisions within modules, facilitating change. The Ada type system supports localization of data definitions so that consistent changes are easy to make. The Ada inheritance features support type extension so that data definitions and interfaces may be customized for an application. Generic units directly support the development of general purpose, adaptable code that can be instantiated to perform specific functions. The Ada 95 improvements for object-oriented techniques and abstraction support all of the above goals. Using these features carefully and in conformance to the guidelines in this book, produces code that is more likely to be reusable.

Reusable code is developed in many ways. Code may be scavenged from a previous project. A reusable library of code may be developed from scratch for a particularly well-understood domain, such as a math library. Reusable code may be developed as an intentional byproduct of a specific application. Reusable code may be developed a certain way because a design method requires it. These guidelines are intended to apply in all of these situations.

The experienced programmer recognizes that software reuse is much more a requirements and design issue than a coding issue. The guidelines in this section are intended to work within an overall method for developing reusable code. This section will not deal with artifacts of design, testing, etc. Some research into reuse issues related specifically to the Ada language can be found in AIRMICS (1990), Edwards (1990), and Wheeler (1992).

Regardless of development method, experience indicates that reusable code has certain characteristics, and this chapter makes the following assumptions:

In addition to these criteria, a reusable part must be easier to reuse than to reinvent, must be efficient, and must be portable. If it takes more effort to reuse a part than to create one from scratch or if the reused part is simply not efficient enough, reuse does not occur as readily. For guidelines on portability, see Chapter 7.

This chapter should not be read in isolation. In many respects, a well-written, reusable component is simply an extreme example of a well-written component. All of the guidelines in the previous chapters and in Chapter 9 apply to reusable components as well as components specific to a single application. As experience increases with the 1995 revision to the Ada standard, new guidelines may emerge while others may change. The guidelines listed here apply specifically to reusable components.

Guidelines in this chapter are frequently worded "consider . . ." because hard and fast rules cannot apply in all situations. The specific choice you can make in a given situation involves design tradeoffs. The rationale for these guidelines is intended to give you insight into some of these tradeoffs.

8.1 UNDERSTANDING AND CLARITY

It is particularly important that parts intended for reuse should be easy to understand. What the part does, how to use it, what anticipated changes might be made to it in the future, and how it works are facts that must be immediately apparent from inspection of the comments and the code itself. For maximum readability of reusable parts, follow the guidelines in Chapter 3, some of which are repeated more strongly below.

8.1.1 Application-Independent Naming

guideline

example

General-purpose stack abstraction:
------------------------------------------------------------------------
generic
   type Item is private;
package Bounded_Stack is
   procedure Push (New_Item    : in     Item);
   procedure Pop  (Newest_Item :    out Item);
   ...
end Bounded_Stack;
------------------------------------------------------------------------

Renamed appropriately for use in current application:

with Bounded_Stack;

...

   type Tray is ...
   package Tray_Stack is 
      new Bounded_Stack (Item => Tray);

rationale

Choosing a general or application-independent name for a reusable part encourages its wide reuse. When the part is used in a specific context, it can be instantiated (if generic) or renamed with a more specific name.

When there is an obvious choice for the simplest, clearest name for a reusable part, it is a good idea to leave that name for use by the reuser of the part, choosing a longer, more descriptive name for the reusable part. Thus, Bounded_Stack is a better name than Stack for a generic stack package because it leaves the simpler name Stack available to be used by an instantiation.

Include indications of the behavioral characteristics (but not indications of the implementation) in the name of a reusable part so that multiple parts with the same abstraction (e.g., multiple stack packages) but with different restrictions (bounded, unbounded, etc.) can be stored in the same Ada library and used as part of the same Ada program.

8.1.2 Abbreviations

guideline

example
------------------------------------------------------------------------
with Ada.Calendar;
package Greenwich_Mean_Time is
   function Clock return Ada.Calendar.Time;
   ...
end Greenwich_Mean_Time;
------------------------------------------------------------------------

The following abbreviation may not be clear when used in an application:

with Ada.Calendar;
with Greenwich_Mean_Time;
...
   function Get_GMT return Ada.Calendar.Time renames
          Greenwich_Mean_Time.Clock;

rationale

This is a stronger guideline than Guideline 3.1.4. However well commented, an abbreviation may cause confusion in some future reuse context. Even universally accepted abbreviations, such as GMT for Greenwich Mean Time, can cause problems and should be used only with great caution.

The difference between this guideline and Guideline 3.1.4 involves issues of domain. When the domain is well-defined, abbreviations and acronyms that are accepted in that domain will clarify the meaning of the application. When that same code is removed from its domain-specific context, those abbreviations may become meaningless.

In the example above, the package, Greenwich_Mean_Time, could be used in any application without loss of meaning. But the function Get_GMT could easily be confused with some other acronym in a different domain.

notes

See Guideline 5.7.2 concerning the proper use of the renames clause. If a particular application makes extensive use of the Greenwich_Mean_Time domain, it may be appropriate to rename the package GMT within that application:
with Greenwich_Mean_Time;
...
   package GMT renames Greenwich_Mean_Time;

8.1.3 Generic Formal Parameters

guideline

example

The following example shows how a very general algorithm can be developed but must be clearly documented to be used:
------------------------------------------------------------------------
generic
   -- Index provides access to values in a structure.  For example,
   -- an array, A.
   type Index is (<>);
   type Element is private;
   type Element_Array is array (Index range <>) of Element;
   -- The function, Should_Precede, does NOT compare the indexes
   -- themselves; it compares the elements of the structure.
   -- The function Should_Precede is provided rather than a "Less_Than" function
   -- because the sort criterion need not be smallest first.
   with function Should_Precede (Left  : in     Element;
                                 Right : in     Element)
     return Boolean;
   -- This procedure swaps values of the structure (the mode won't
   -- allow the indexes themselves to be swapped!)
   with procedure Swap (Index1 : in     Index;
                        Index2 : in     Index;
                        A      : in out Element_Array);
   -- After the call to Quick_Sort, the indexed structure will be
   -- sorted:
   --     For all i,j in First..Last :  i<j  =>  A(i) < A(j).
procedure Quick_Sort (First : in     Index := Index'First;
                      Last  : in     Index := Index'Last);
------------------------------------------------------------------------

rationale

The generic capability is one of Ada's strongest features because of its formalization. However, not all of the assumptions made about generic formal parameters can be expressed directly in Ada. It is important that any user of a generic know exactly what that generic needs in order to behave correctly.

In a sense, a generic specification is a contract where the instantiator must supply the formal parameters and, in return, receives a working instance of the specification. Both parties are best served when the contract is complete and clear about all assumptions.

8.2 ROBUSTNESS

The following guidelines improve the robustness of Ada code. It is easy to write code that depends on an assumption that you do not realize that you are making. When such a part is reused in a different environment, it can break unexpectedly. The guidelines in this section show some ways in which Ada code can be made to automatically conform to its environment and some ways in which it can be made to check for violations of assumptions. Finally, some guidelines are given to warn you about errors that Ada does not catch as soon as you might like.

8.2.1 Named Numbers

guideline

example
------------------------------------------------------------------------
procedure Disk_Driver is
   -- In this procedure, a number of important disk parameters are
   -- linked.
   Number_Of_Sectors  : constant :=     4;
   Number_Of_Tracks   : constant :=   200;
   Number_Of_Surfaces : constant :=    18;
   Sector_Capacity    : constant := 4_096;
   Track_Capacity   : constant := Number_Of_Sectors  * Sector_Capacity;
   Surface_Capacity : constant := Number_Of_Tracks   * Track_Capacity;
   Disk_Capacity    : constant := Number_Of_Surfaces * Surface_Capacity;
   type Sector_Range  is range 1 .. Number_Of_Sectors;
   type Track_Range   is range 1 .. Number_Of_Tracks;
   type Surface_Range is range 1 .. Number_Of_Surfaces;
   type Track_Map   is array (Sector_Range)  of ...;
   type Surface_Map is array (Track_Range)   of Track_Map;
   type Disk_Map    is array (Surface_Range) of Surface_Map;
begin  -- Disk_Driver
   ...
end Disk_Driver;
------------------------------------------------------------------------

rationale

To reuse software that uses named numbers and static expressions appropriately, just one or a small number of constants need to be reset, and all declarations and associated code are changed automatically. Apart from easing reuse, this reduces the number of opportunities for error and documents the meanings of the types and constants without using error-prone comments.

8.2.2 Unconstrained Arrays

guideline

example
   ...
   type Vector is array (Vector_Index range <>) of Element;
   type Matrix is array
           (Vector_Index range <>, Vector_Index range <>) of Element;
   ...
   ---------------------------------------------------------------------
   procedure Matrix_Operation (Data : in     Matrix) is
      Workspace   : Matrix (Data'Range(1), Data'Range(2));
      Temp_Vector : Vector (Data'First(1) .. 2 * Data'Last(1));
   ...
   ---------------------------------------------------------------------

rationale

Unconstrained arrays can be declared with their sizes dependent on formal parameter sizes. When used as local variables, their sizes change automatically with the supplied actual parameters. This facility can be used to assist in the adaptation of a part because necessary size changes in local variables are taken care of automatically.

8.2.3 Minimizing and Documenting Assumptions

guideline

example

The following poorly written function documents but does not check its assumption:
   -- Assumption:  BCD value is less than 4 digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD;

The next example enforces conformance with its assumption, making the checking automatic and the comment unnecessary:

   subtype Binary_Values is Natural range 0 .. 9_999;
   function Binary_To_BCD (Binary_Value : in     Binary_Values)
     return BCD;

The next example explicitly checks and documents its assumption:

   ---------------------------------------------------------------------
   -- Out_Of_Range raised when BCD value exceeds 4  digits.
   function Binary_To_BCD (Binary_Value : in     Natural)
     return BCD is
      Maximum_Representable : constant Natural := 9_999;
   begin  -- Binary_To_BCD
      if Binary_Value > Maximum_Representable then
         raise Out_Of_Range;
      end if;
      ...
   end Binary_To_BCD;
   ---------------------------------------------------------------------

rationale

Any part that is intended to be used again in another program, especially if the other program is likely to be written by other people, should be robust. It should defend itself against misuse by defining its interface to enforce as many assumptions as possible and by adding explicit defensive checks on anything that cannot be enforced by the interface. By documenting dependencies on a Special Needs Annex, you warn the user that he should only reuse the component in a compilation environment that provides the necessary support.

notes

You can restrict the ranges of values of the inputs by careful selection or construction of the subtypes of the formal parameters. When you do so, the compiler-generated checking code may be more efficient than any checks you might write. Indeed, such checking is part of the intent of the strong typing in the language. This presents a challenge, however, for generic units where the user of your code selects the types of the parameters. Your code must be constructed to deal with any value of any subtype the user may choose to select for an instantiation.

8.2.4 Subtypes in Generic Specifications

guideline

example

In the following example, it appears that any value supplied for the generic formal object Object would be constrained to the range 1..10. It also appears that parameters passed at run-time to the Put routine in any instantiation and values returned by the Get routine would be similarly constrained:
   subtype Range_1_10 is Integer range 1 .. 10;
   ---------------------------------------------------------------------
   generic
      Object : in out Range_1_10;
      with procedure Put (Parameter : in     Range_1_10);
      with function  Get return Range_1_10;
   package Input_Output is
      ...
   end Input_Output;
   ---------------------------------------------------------------------

However, this is not the case. Given the following legal instantiation:

   subtype Range_15_30 is Integer range 15 .. 30;
   Constrained_Object : Range_15_30 := 15;
   procedure Constrained_Put (Parameter : in     Range_15_30);
   function  Constrained_Get return Range_15_30;
   package Constrained_Input_Output is
      new Input_Output (Object => Constrained_Object,
                        Put    => Constrained_Put,
                        Get    => Constrained_Get);
   ...

Object, Parameter, and the return value of Get are constrained to the range 15..30. Thus, for example, if the body of the generic package contains an assignment statement:

Object := 1;

Constraint_Error is raised when this instantiation is executed.

rationale

The language specifies that when constraint checking is performed for generic formal objects and parameters and return values of generic formal subprograms, the constraints of the actual subtype (not the formal subtype) are enforced (Ada Reference Manual 1995, §§12.4 and 12.6).Thus, the subtype specified in a formal in out object parameter and the subtypes specified in the profile of a formal subprogram need not match those of the actual object or subprogram.

Thus, even with a generic unit that has been instantiated and tested many times and with an instantiation that reported no errors at instantiation time, there can be a run-time error. Because the subtype constraints of the generic formal are ignored, the Ada Reference Manual (1995, §§12.4 and 12.6) suggests using the name of a base type in such places to avoid confusion. Even so, you must be careful not to assume the freedom to use any value of the base type because the instantiation imposes the subtype constraints of the generic actual parameter. To be safe, always refer to specific values of the type via symbolic expressions containing attributes like 'First, 'Last, 'Pred, and 'Succ rather than via literal values.

For generics, attributes provide the means to maintain generality. It is possible to use literal values, but literals run the risk of violating some constraint. For example, assuming that an array's index starts at 1 may cause a problem when the generic is instantiated for a zero-based array type.

notes

Adding a generic formal parameter that defines the subtype of the generic formal object does not address the ramifications of the constraint checking rule discussed in the above rationale. You can instantiate the generic formal type with any allowable subtype, and you are not guaranteed that this subtype is the first subtype:
generic
   type Object_Range is range <>;
   Objects : in out Object_Range;
   ...
package X is
   ...
end X;

You can instantiate the subtype Object_Range with any Integer subtype, for example, Positive. However, the actual variable Object can be of Positive'Base, i.e., Integer and its value are not guaranteed to be greater than 0.

8.2.5 Overloading in Generic Units

guideline

example
------------------------------------------------------------------------
generic
   type Item is limited private;
package Input_Output is
   procedure Put (Value : in     Integer);
   procedure Put (Value : in     Item);
end Input_Output;
------------------------------------------------------------------------

rationale

If the generic package shown in the example above is instantiated with Integer (or any subtype of Integer) as the actual type corresponding to generic formal Item, then the two Put procedures have identical interfaces, and all calls to Put are ambiguous. Therefore, this package cannot be used with type Integer. In such a case, it is better to give unambiguous names to all subprograms. See the Ada Reference Manual (1995, §12.3) for more information.

8.2.6 Hidden Tasks

guideline

rationale

The effects of tasking become a major factor when reusable code enters the domain of real-time systems. Even though tasks may be used for other purposes, their effect on scheduling algorithms is still a concern and must be clearly documented. With the task clearly documented, the real-time programmer can then analyze performance, priorities, and so forth to meet timing requirements, or, if necessary, he can modify or even redesign the component.

Concurrent access to data structures must be carefully planned to avoid errors, especially for data structures that are not atomic (see Chapter 6 for details). If a generic unit accesses one of its generic formal parameters (reads or writes the value of a generic formal object or calls a generic formal subprogram that reads or writes data) from within a task contained in the generic unit, then there is the possibility of concurrent access for which the user may not have planned. In such a case, the user should be warned by a comment in the generic specification.

8.2.7 Exceptions

guideline

example
------------------------------------------------------------------------
generic
   type Number is limited private;
   with procedure Get (Value :    out Number);
procedure Process_Numbers;

------------------------------------------------------------------------
procedure Process_Numbers is
   Local : Number;
   procedure Perform_Cleanup_Necessary_For_Process_Numbers is separate;
   ...
begin  -- Process_Numbers
   ...
   Catch_Exceptions_Generated_By_Get:
      begin
         Get (Local);
      exception
         when others =>
            Perform_Cleanup_Necessary_For_Process_Numbers;
            raise;
      end Catch_Exceptions_Generated_By_Get;
   ...
end Process_Numbers;
------------------------------------------------------------------------

rationale

On most occasions, an exception is raised because an undesired event (such as floating-point overflow) has occurred. Such events often need to be dealt with entirely differently with different uses of a particular software part. It is very difficult to anticipate all the ways that users of the part may wish to have the exceptions handled. Passing the exception out of the part is the safest treatment.

In particular, when an exception is raised by a generic formal subprogram, the generic unit is in no position to understand why or to know what corrective action to take. Therefore, such exceptions should always be propagated back to the caller of the generic instantiation. However, the generic unit must first clean up after itself, restoring its internal data structures to a correct state so that future calls may be made to it after the caller has dealt with the current exception. For this reason, all calls to generic formal subprograms should be within the scope of a when others exception handler if the internal state is modified, as shown in the example above.

When a reusable part is invoked, the user of the part should be able to know exactly what operation (at the appropriate level of abstraction) has been performed. For this to be possible, a reusable part must always do all or none of its specified function; it must never do half. Therefore, any reusable part that terminates early by raising or propagating an exception should return to the caller with no effect on the internal or external state. The easiest way to do this is to test for all possible exceptional conditions before making any state changes (modifying internal state variables, making calls to other reusable parts to modify their states, updating files, etc.). When this is not possible, it is best to restore all internal and external states to the values that were current when the part was invoked before raising or propagating the exception. Even when this is not possible, it is important to document this potentially hazardous situation in the comment header of the specification of the part.

A similar problem arises with parameters of mode out or in out when exceptions are raised. The Ada language distinguishes between "by-copy" and "by-reference" parameter passing. In some cases, "by-copy" is required; in other cases, "by-reference" is required; and in the remaining cases, either mechanism is allowed. The potential problem arises in those cases where the language does not specify the parameter passing mechanism to use. When an exception is raised, the copy-back does not occur, but for an Ada compiler, which passes parameters by reference (in those cases where a choice is allowed), the actual parameter has already been updated. When parameters are passed by copy, the update does not occur. To reduce ambiguity, increase portability, and avoid situations where some but not all of the actual parameters are updated when an exception is raised, it is best to treat values of out and in out parameters like state variables, updating them only after it is certain that no exception will be raised.

See also Guideline 7.1.8.

notes

A reusable part could range from a low-level building block (e.g., data structure, sorting algorithm, math function) to a large reusable subsystem. The lower level the building block, the less likely that the reusable part will know how to handle exceptions or produce meaningful results. Thus, the low-level parts should propagate exceptions. A large reusable subsystem, however, should be able to handle any anticipated exceptions independently of the variations across which it is reused.

8.3 ADAPTABILITY

Reusable parts often need to be changed before they can be used in a specific application. They should be structured so that change is easy and as localized as possible. One way of achieving adaptability is to create general parts with complete functionality, only a subset of which might be needed in a given application. Another way to achieve adaptability is to use Ada's generic construct to produce parts that can be appropriately instantiated with different parameters. Both of these approaches avoid the error-prone process of adapting a part by changing its code but have limitations and can carry some overhead.

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 changes 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.

8.3.1 Complete Functionality

guideline

example
   Incoming : Queue;
   ...
   Set_Initial (Incoming);     -- initialization operation
   ...
   if Is_Full (Incoming) then  -- query operation
      ...
   end if;
   ...
   Clean_Up (Incoming);        -- finalization operation

rationale

This functionality is particularly important in designing/programming an abstraction. You have to balance the completeness of the abstraction against its extensibility. Completeness ensures that you have configured the abstraction correctly, without built-in assumptions about its execution environment. It also ensures the proper separation of functions so that they are useful to the current application and, in other combinations, to other applications. Extensibility ensures that reusers can add functionality by extension, using tagged type hierarchies (see Guideline 8.4.8 and Chapter 9) or child library packages (see Guidelines 4.1.6, 8.4.1, and 9.4.1).

In designing for reuse, you need to think in terms of clean abstractions. If you provide too little functionality and rely on your reusers to extend the abstraction, they risk having an abstraction that lacks cohesion. This hodgepodge abstraction has inherited many operations, not all of which are necessary or work together.

When a reusable part can be implemented reasonably 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.

The predefined types Ada.Finalization.Controlled or Ada.Finalization.Limited_Controlled provide automatic, user-definable initialization, adjustment, and finalization procedures. When you declare controlled types and objects, you are guaranteed that the compiler will insert the necessary calls to initialization, adjustment, and finalization, making your code less error-prone and more maintainable. When overriding the Initialize and Finalize routines on the controlled types, make sure to call the parent Initialize or Finalize.

notes

The example illustrates end condition functions. An abstraction should be automatically initialized before its user gets a chance to damage it. When that is not possible, it should be supplied with initialization operations. In any case, it needs finalization operations. One way to supply the initialization and finalization operations is to derive the abstraction from the predefined types Ada.Finalization.Controlled or Ada.Finalization.Limited_Controlled. Wherever possible, query operations should be provided to determine when limits are about to be exceeded, so that the user can avoid causing exceptions to be raised.

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.5). 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.

8.3.2 Generic Units

guideline

rationale

Ada does not allow data types to be passed as actual parameters to subprograms during execution. Such parameters must be specified as generic formal parameters to a generic unit when it is instantiated. Therefore, if you want to write a subprogram for which there is variation from call to call in the data type of objects on which it operates, then you must write the subprogram as a generic unit and instantiate it once for each combination of data type parameters. The instantiations of the unit can then be called as regular subprograms.

You can pass subprograms as actual parameters either by declaring access-to-subprogram values or generic formal subprogram parameters. See Guideline 5.3.4 for a discussion of the tradeoffs.

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 that 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 that 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.

See also Guideline 9.3.5.

8.3.3 Formal Private and Limited Private Types

guideline

example

The first example shows a case of a template providing only a data structure, a case in which assignment is clearly not needed in the body of the generic:
------------------------------------------------------------------------
generic
   type Element_Type is limited private;
package Generic_Doubly_Linked_Lists is
   type Cell_Type;
   type List_Type is access all Element_Type;
   type Cell_Type is
      record
         Data     : Element_Type;
         Next     : List_Type;
         Previous : List_Type;
      end record;
end Generic_Doubly_Linked_Lists;

The second example shows a template that composes new operations out of (nonassignment) operations passed as generic formal parameters:

generic
   type Element_Type is limited private;
   with procedure Process_Element (X : in out Element_Type);
   type List_Type is array (Positive range <>) of Element_Type;
procedure Process_List (L : in out List_Type);
procedure Process_List (L : in out List_Type) is
begin -- Process_List
   for I in L'Range loop
      Process_Element (L(I));
   end loop;
end Process_List;
------------------------------------------------------------------------
generic
   type Domain_Type is limited private;
   type Intermediate_Type is limited private;
   type Range_Type is limited private;
   with function Left (X : Intermediate_Type) return Range_Type;
   with function Right (X : Domain_Type) return Intermediate_Type;
function Generic_Composition (X : Domain_Type) return Range_Type;
-- the function Left o Right
function Generic_Composition (X : Domain_Type) return Range_Type is
begin  -- generic_Composition
   return Left (Right (X));
end Generic_Composition;

The third example shows how to use Ada's controlled types to provide special assignment semantics:

with Ada.Finalization;
generic
   type Any_Element is new Ada.Finalization.Controlled with private;
   Maximum_Stack_Size : in Natural := 100;
package Bounded_Stack is
   type Stack is private;
   procedure Push (On_Top      : in out Stack;
                   New_Element : in     Any_Element);
   procedure Pop  (From_Top    : in out Stack;
                   Top_Element :    out Any_Element);
   Overflow  : exception;
   Underflow : exception;
   ...
private
   type Stack_Information;
   type Stack is access Stack_Information;
end Bounded_Stack;

rationale

For a generic component to be usable in as many contexts as possible, it should minimize the assumptions that it makes about its environment and should make explicit any assumptions that are necessary. In Ada, the assumptions made by generic units can be stated explicitly by the types of the generic formal parameters. A limited private generic formal type prevents the generic unit from making any assumptions about the structure of objects of the type or about operations defined for such objects. A private (nonlimited) generic formal type allows the assumption that assignment and equality comparison operations are defined for the type. Thus, a limited private data type cannot be specified as the actual parameter for a private generic formal type.

In general, you should choose the private or limited private generic formal type based on the need for assignment inside a generic. Limited private types should be used for abstractions that do not need assignment, as in the first two examples above. In the third example, where assignment is needed, a type derived from a controlled type is specified to ensure that the correct assignment semantics will be available. If you need equality in the body of the generic, you may need to redefine equality as well to get the correct semantics; you would then need to include a formal generic subprogram parameter for the = function.

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.

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 a case where it makes sense to allow the user to make copies of and compare data objects and when the underlying data type involves access types (so that the entire data structure gets copied or compared), then it is better to export a controlled type and an (overridden) equality operation. 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.

One use of generic units is to create a mixin generic (see Guideline 8.3.8) to extend a tagged type. In this situation, you want to use the most restrictive type as the generic formal type, that is, a formal type that is both limited and abstract. When you instantiate the generic, if the actual type is nonlimited, the type extension will also be nonlimited. In the generic package, you must declare the type extension as abstract. The instantiator of the generic can then extend the type again to achieve the desired mixin configuration.

notes

The predefined packages, Sequential_IO and Direct_IO, take private types. This will complicate I/O requirements for limited private types and should be considered during design.

There are also some cases where you must use a limited private formal type. These cases arise when the formal type has an access discriminant, or the formal is used as the parent type in defining a type extension that itself includes a component of a limited type (e.g., task type), or the formal defines a new discriminant part with an access discriminant.

8.3.4 Using Generic Units to Encapsulate Algorithms

guideline

example

This is the specification of a generic sort procedure:
------------------------------------------------------------------------
generic
   type Element is private;
   type Data    is array (Positive range <>) of Element;
   with function Should_Precede (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 Should_Precede (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;
   function Should_Precede (Left  : in     Integer;
                            Right : in     Integer)
     return Boolean;

   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;
   function Should_Precede (Left  : in     String_80;
                            Right : in     String_80)
     return Boolean;

   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);

rationale

A sort algorithm can be described independently of the data type being sorted. This generic procedure takes the 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: Should_Precede 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.

Chapter 8 Continued