[Ada Information Clearinghouse]

Ada '83 Quality and Style:

Guidelines for Professional Programmers

Copyright 1989, 1991,1992 Software Productivity Consortium, Inc., Herndon, Virginia.

CHAPTER 8: Reusability

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

In this section...
8.3.1 Complete Functionality
8.3.2 Generic Units
8.3.3 Using Generic Units to Encapsulate Algorithms
8.3.4 Using Generic Units for Abstract Data Types
8.3.5 Iterators
8.3.6 Private and Limited Private Types
Summary of Guidelines from this section


8.3.1 Complete Functionality

guideline

example

   Incoming : Queue; 
   ...
   
   Initialize(Incoming);      -- initialization operation 
   ...
   
   if Is_Full(Incoming) then  -- query operation 
      ... 
   end if;
   
   ...
   
   Finalize(Incoming);        -- finalization operation

rationale

This is particularly important in designing/programming an abstraction. 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. It is particularly important that they be available to other applications; remember that they can be "optimized" out of the final version of the current product.

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.

note

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


8.3.2 Generic Units

guideline

rationale

Ada does not allow subprograms or 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, or in the subprogram which it calls, then you must write the subprogram as a generic unit and instantiate it once for each combination of data type and subprogram parameters. The instantiations of the unit can then be called as regular subprograms.

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


8.3.3 Using Generic Units to Encapsulate Algorithms

guideline

example

This is the specification of a generic sort procedure:
------------------------------------------------------------------------ 
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);

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: 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


8.3.4 Using Generic Units for Abstract Data Types

guideline

example

This example presents a series of different techniques which can be used to generate abstract data types and objects. A discussion of the merits of each follows in the rationale section below. The first is an abstract data object (ADO), also known as an abstract state machine (ASM). It encapsulates one stack of integers.
------------------------------------------------------------------------ 
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; 
------------------------------------------------------------------------

rationale

The biggest advantage of an ADT over an ADO (or a GADT over a GADO) is that the user of the package can declare as many objects as desired with an ADT. These objects can be declared as standalone variables or as components of arrays and records. They can also be passed as parameters. None of this is possible with an ADO, where the single data object is encapsulated inside of the package. Furthermore, an ADO provides no more protection of the data structure than an ADT. When a private type is exported by the ADT package, as in the example above, then for both the ADO and ADT, the only legal operations which can modify the data are those defined explicitly by the package (in this case, 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.

note

The predefined simple types will need an assign or equality operation when used to instantiate generics that expect limited private types. Although it is a nuisance, it is simple enough for the few times it would apply.

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


8.3.5 Iterators

guideline

example

The following package defines an abstract list data type, with both active and passive iterators for traversing a list.
------------------------------------------------------------------------ 
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; 
------------------------------------------------------------------------

rationale

Iteration over complex data structures is often required and, if not provided by the part itself, can be difficult to implement without violating information hiding principles.

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.

note

For further discussion of passive and active iterators see Ross (1989) and Booch (1987).

Language Ref Manual references: 2.7 Comments, 12.1 Generic Declarations


8.3.6 Private and Limited Private Types

guideline

example

The first example violates the guideline by having private (nonlimited) generic formal types.
------------------------------------------------------------------------ 
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; 
------------------------------------------------------------------------

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.

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.

note

It is possible (but clumsy) to redefine equality for nonlimited types. However, if a generic imports a (nonlimited) private type and uses equality, it will automatically use the predefined equality and not the user-supplied redefinition. This is another argument for using limited private generic formal parameters.

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


Back to document index