Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

8.2 Lists and vectors

We will first consider the list container since in some ways it is the simplest. Here is its specification interspersed with some explanation
generic
   type Element_Type is private;
   with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Doubly_Linked_Lists is
   pragma Preelaborate(Doubly_Linked_Lists);
   type List is tagged private;
   pragma Preelaborable_Initialization(List);
   type Cursor is private;
   pragma Preelaborable_Initialization(Cursor);
   Empty_List: constant List;
   No_Element: constant Cursor;
The two generic parameters are the type of the elements in the list and the definition of equality for comparing elements. This equality relation must be such that x = y and y = x always have the same value.
A list container is an object of the type List. It is tagged since it will inevitably be implemented as a controlled type. The fact that it is visibly tagged means that all the advantages of object oriented programming are available. For one thing it enables the use of the prefixed notation so that we can write operations such as 
My_List.Append(Some_Value);
rather than 
Append(My_List, Some_Value);
The type Cursor is an important concept. It provides the means of access to individual elements in the container. Not only does it contain a reference to an element but it also identifies the container as well. This enables various checks to be made to ensure that we don't accidentally meddle with an element in the wrong container.
The constants Empty_List and No_Element are as expected and also provide default values for objects of types List and Cursor respectively.
function "=" (Left, Right: List) return Boolean;
function Length(Container: List) return Count_Type;
function Is_Empty(Container: List) return Boolean;
procedure Clear(Container: in out List);
The function "=" compares two lists. It only returns true if both lists have the same number of elements and corresponding elements have the same value as determined by the generic parameter "=" for comparing elements. The subprograms Length, Is_Empty and Clear are as expected.
Note that A_List = Empty_List, Is_Empty(A_List) and Length(A_List) = 0 all have the same value.
function Element(Position: Cursor) return Element_Type;
procedure Replace_Element(
      Container: in out List; Position: in Cursor;
      New_Item: in Element_Type);
These are the first operations we have met that use a cursor. The function Element takes a cursor and returns the value of the corresponding element (remember that a cursor identifies the list as well as the element itself). The procedure Replace_Element replaces the value of the element identified by the cursor by the value given; it makes a copy of course.
Note carefully that Replace_Element has both the list and cursor as parameters. There are two reasons for this concerning correctness. One is to enable a check that the cursor does indeed identify an element in the given list. The other is to ensure that we do have write access to the container (the parameter has mode in out). Otherwise it would be possible to modify a container even though we only had a constant view of it. So as a general principle any operation that modifies a container must have the container as a parameter whereas an operation that only reads it such as the function Element does not.
procedure Query_Element(
      Position: in Cursor;
      Process: not null access procedure (Element: in Element_Type));
procedure Update_Element(
      Container: in out List; Position: in Cursor;
      Process: not null access procedure (Element: in out Element_Type));
These procedures provide in situ access to an element. One parameter is the cursor identifying the element and another is an access to a procedure to be called with that element as parameter. In the case of Query_Element, we can only read the element whereas in the case of Update_Element we can change it as well since the parameter mode of the access procedure is in out. Note that Update_Element also has the container as a parameter for reasons just mentioned when discussing Replace_Element.
The reader might wonder whether there is any difference between calling the function Element to obtain the current value of an element and using the seemingly elaborate mechanism of Query_Element. The answer is that the function Element makes a copy of the value whereas Query_Element gives access to the value without making a copy. (And similarly for Replace_Element and Update_Element.) This wouldn't matter for a simple list of integers but it would matter if the elements were large or of a controlled type (maybe even lists themselves).
procedure Move(Target, Source: in out List);
This moves the list from the source to the target after first clearing the target. It does not make copies of the elements so that after the operation the source is empty and Length(Source) is zero.
procedure Insert(
      Container: in out List;
      Before: in Cursor;
      New_Item: in Element_Type;
      Count: in Count_Type := 1);
procedure Insert(
      Container: in out List;
      Before: in Cursor;
      New_Item: in Element_Type;
      Position: out Cursor;
      Count: in Count_Type := 1);
procedure Insert(
      Container: in out List;
      Before: in Cursor;
      Position: out Cursor;
      Count: in Count_Type := 1);
These three procedures enable one or more identical elements to be added anywhere in a list. The place is indicated by the parameter Before – if this is No_Element, then the new elements are added at the end. The second procedure is similar to the first but also returns a cursor to the first of the added elements. The third is like the second but the new elements take their default values. Note the default value of one for the number of elements.
procedure Prepend(
      Container: in out List;
      New_Item: in Element_Type;
      Count: in Count_Type := 1);
procedure Append(
      Container: in out List;
      New_Item: in Element_Type;
      Count: in Count_Type := 1);
These add one or more new elements at the beginning or end of a list respectively. Clearly these operations can be done using Insert but they are sufficiently commonly needed that it is convenient to provide them specially.
procedure Delete(
      Container: in out List;
      Position: in out Cursor;
      Count: in Count_Type := 1);
procedure Delete_First(Container: in out List; Count: in Count_Type := 1);
procedure Delete_Last(Container: in out List; Count: in Count_Type := 1);
These delete one or more elements at the appropriate position. In the case of Delete, the parameter Position is set to No_Element upon return. If there are not as many as Count elements to be deleted at the appropriate place then it just deletes as many as possible (this clearly results in the container becoming empty in the case of Delete_First and Delete_Last).
procedure Reverse_Elements(Container: in out List);
This does the obvious thing. It would have been nice to call this procedure Reverse but sadly that is a reserved word.
procedure Swap(Container: in out List; I, J: in Cursor);
This handy procedure swaps the values in the two elements denoted by the two cursors. The elements must be in the given container otherwise Program_Error is raised. Note that the cursors do not change.
procedure Swap_Links(Container: in out List; I, J: in Cursor);
This performs the low level operation of swapping the links rather than the values which can be much faster if the elements are large. There is no analogue in the vectors package.
procedure Splice(
      Target: in out List;
      Before: in Cursor;
      Source in out List);
procedure Splice(
      Target: in out List;
      Before: in Cursor;
      Source: in out List;
      Position: in out Cursor);
procedure Splice(
      Container: in out List;
      Before: in Cursor;
      Position: in Cursor);
These three procedures enable elements to be moved (without copying). The place is indicated by the parameter Before – if this is No_Element, then the elements are added at the end. The first moves all the elements of Source into Target at the position given by Before; as a consequence, like the procedure Move, after the operation the source is empty and Length(Source) is zero. The second moves a single element at Position from the list Source to Target and so the length of target is incremented whereas that of source is decremented; Position is updated to its new location in Target. The third moves a single element within a list and so the length remains the same (note the formal parameter is Container rather than Target in this case). There are no corresponding operations in the vectors package because, like Swap_Links, we are just moving the links and not copying the elements.
function First(Container: List) return Cursor;
function First_Element(Container: List) return Element_Type;
function Last(Container: List) return Cursor;
function Last_Element(Container: List) return Element_Type;
function Next(Position: Cursor) return Cursor;
function Previous(Position: Cursor) return Cursor;
procedure Next(Position: in out Cursor);
procedure Previous(Position: in out Cursor);
function Find(
      Container: List;
      Item: Element_Type;
      Position: Cursor:= No_Element) return Cursor;
function Reverse_Find(
      Container: List;
      Item: Element_Type;
      Position: Cursor:= No_Element) return Cursor;
function Contains(Container: List; Item: Element_Type) return Boolean;
Hopefully the purpose of these is almost self-evident. The function Find searches for an element with the given value starting at the given cursor position (or at the beginning if the position is No_Element); if no element is found then it returns No_Element. Reverse_Find does the same but backwards. Note that equality used for the comparison in Find and Reverse_Find is that defined by the generic parameter "=".
function Has_Element(Position: Cursor) return Boolean;
This returns False if the cursor does not identify an element; for example if it is No_Element.
procedure Iterate(
      Container: in List;
      Process: not null access procedure (Position: in Cursor));
procedure Reverse_Iterate(
      Container: in List;
      Process: not null access procedure (Position: in Cursor));
These apply the procedure designated by the parameter Process to each element of the container in turn in the appropriate order.
generic
   with function "<" (Left, Right: Element_Type) return Boolean is <>;
package Generic_Sorting is
   function Is_Sorted(Container: List) return Boolean;
   procedure Sort(Container: in out List);
   procedure Merge(Target, Source: in out List);
end Generic_Sorting;
This generic package performs sort and merge operations using the order specified by the generic formal parameter. Note that we use generics rather than access to subprogram parameters when the formal process is given by an operator. This is because the predefined operations have convention Intrinsic and one cannot pass an intrinsic operation as an access to subprogram parameter. The function Is_Sorted returns True if the container is already sorted. The procedure Sort arranges the elements into order as necessary – note that no copying is involved since it is only the links that are moved. The procedure Merge takes the elements from Source and adds them to Target. After the merge Length(Source) is zero. If both lists were sorted before the merge then the result is also sorted.
And finally we have
private
   ... -- not specified by the language
end Ada.Containers.Doubly_Linked_Lists;
If the reader has got this far they have probably understood how to use this package so extensive examples are unnecessary. However, as a taste, here is a simple stack of floating point numbers 
package Stack is
   procedure Push(X: in Float);
   function Pop return Float;
   function Size return Integer;
   Stack_Empty : exception;
end;
with Ada.Containers.Doubly_Linked_Lists;
use Ada.Containers;
package body Stack is
   package Float_Container is new Doubly_Linked_Lists(Float);
   use Float_Container;
   The_Stack: List;
   procedure Push(X: in Float) is
   begin
      Append(The_Stack, X);    -- or The_Stack.Append(X);
   end Push;
   function Pop return Float is
      Result: Float;
   begin
      if Is_Empty(The_Stack) then
         raise Stack_Empty;
      end if;
      Result := Last_Element(The_Stack);
      Delete_Last(The_Stack);
      return Result;
   end Pop;
   function Size return Integer is
   begin
      return Integer(Length(The_Stack));
   end Size;
end Stack;
This barely needs any explanation. The lists package is instantiated in the package Stack and the object The_Stack is of course the list container. The rest is really straightforward. We could of course use the prefixed notation throughout as indicated in Push.
An important point should be mentioned concerning lists (and containers in general). This is that attempts to do foolish things typically result in Constraint_Error or Program_Error being raised. This especially applies to the procedures Process in Query_Element, Update_Element, Iterate and Reverse_Iterate. The concepts of tampering with cursors and elements are introduced in order to dignify a general motto of "Thou shalt not violate thy container".
Tampering with cursors occurs when elements are added to or deleted from a container (by calling Insert and so on) whereas tampering with elements means replacing an element (by calling Replace_Element for example). Tampering with elements is a greater sin and includes tampering with cursors. The procedure Process in Query_Element and Update_Element must not tamper with elements and the procedure Process in the other cases must not tamper with cursors. The reader might think it rather odd that Update_Element should not be allowed to tamper with elements since the whole purpose is to update the element; this comes back to the point mentioned earlier that update element gives access to the existing element in situ via the parameter of Process and that is allowed – calling Replace_Element within Process would be tampering. Tampering causes Program_Error to be raised.
We will now consider the vectors package. Its specification starts
generic
   type Index_Type is range <>;
   type Element_Type is private;
   with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Vectors is
   pragma Preelaborate(Vectors);
This is similar to the lists package except for the additional generic parameter Index_Type (note that this is an integer type and not a discrete type). This additional parameter reflects the idea that a vector is essentially an array and we can index directly into an array.
In fact the vectors package enables us to access elements either by using an index or by using a cursor. Thus many operations are duplicated such as 
function Element(Container: Vector; Index: Index_Type) return Element_Type;
function Element(Position: Cursor) return Element_Type;
procedure Replace_Element(
      Container: in out Vector;
      Index: in Index_Type;
      New_Item: in Element_Type);
procedure Replace_Element(
      Container: in out Vector;
      Position: in Cursor;
      New_Item: in Element_Type);
If we use an index then there is always a distinct parameter identifying the vector as well. If we use a cursor then the vector parameter is omitted if the vector is unchanged as is the case with the function Element. Remember that we stated earlier that a cursor identifies both an element and the container but if the container is being changed as in the case of Replace_Element then the container has to be passed as well to ensure write access and to enable a check that the cursor does identify an element in the correct container.
There are also functions First_Index and Last_Index thus 
function First_Index(Container: Vector) return Index_Type;
function Last_Index(Container: Vector) return Extended_Index;
These return the values of the index of the first and last elements respectively. The function First_Index always returns Index_Type'First whereas Last_Index will return No_Index if the vector is empty. The function Length returns Last_Index–First_Index+1 which is zero if the vector is empty. Note that the irritating subtype Extended_Index has to be introduced in order to cope with end values. The constant No_Index has the value Extended_Index'First which is equal to Index_Type'First–1.
There are operations to convert between an index and a cursor thus 
function To_Cursor(Container: Vector; Index: Extended_Index) return Cursor;
function To_Index(Position: Cursor) return Extended_Index;
It is perhaps slightly messier to use the index and vector parameters because of questions concerning the range of values of the index but probably slightly faster and maybe more familiar. And sometimes of course using an index is the whole essence of the problem. In the chapter on access types (see 3.4) we showed a use of the procedure Update_Element to double the values of those elements of a vector whose index was in the range 5 to 10. This would be tedious with cursors.
But an advantage of using cursors is that (provided certain operations are avoided) it is easy to replace the use of vectors by lists.
For example here is the stack package rewritten to use vectors 
with Ada.Containers.Vectors;    -- changed
use Ada.Containers;
package body Stack is
   package Float_Container is new Vectors(Natural,Float);    -- changed
   use Float_Container;
   The_Stack: Vector;    -- changed
   procedure Push(X: in Float) is
   begin
      Append(The_Stack, X);
   end Push;
   -- etc exactly as before
end Stack;
So the changes are very few indeed and can be quickly done with a simple edit.
Note that the index parameter has been given as Natural rather than Integer. Using Integer will not work since attempting to elaborate the subtype Extended_Index would raise Constraint_Error when evaluating Integer'First–1. But in any event it is more natural for the index range of the container to start at 0 (or 1) rather than a large negative value such as Integer'First.
There are other important properties of vectors that should be mentioned. One is that there is a concept of capacity. Vectors are adjustable and will extend if necessary when new items are added. However, this might lead to lots of extensions and copying and so we can set the capacity of a container by calling
procedure Reserve_Capacity(Container: in out Vector; Capacity: in Count_Type);
There is also 
function Capacity(Container: Vector) return Count_Type;
which naturally returns the current capacity. Note that Length(V) cannot exceed Capacity(V) but might be much less.
If we add items to a vector whose length and capacity are the same then no harm is done. The capacity will be expanded automatically by effectively calling Reserve_Capacity internally. So the user does not need to set the capacity although not doing so might result in poorer performance.
There is also the concept of "empty elements". These are elements whose values have not been set. There is no corresponding concept with lists. It is a bounded error to read an empty element. Empty elements arise if we declare a vector by calling 
function To_Vector(Length: Count_Type) return Vector;
as in 
My_Vector: Vector := To_Vector(100);
There is also the much safer 
function To_Vector(New_Item: Element_Type; Length: Count_Type) return Vector;
which sets all the elements to the value New_Item.
There is also a procedure
procedure Set_Length(Container: in out Vector; Length: in Count_Type);
This changes the length of a vector. This may require elements to be deleted (from the end) or to be added (in which case the new ones are empty).
The final way to get an empty element is by calling one of 
procedure Insert_Space(
      Container: in out Vector;
      Before: in Extended_Index;
      Count: in Count_Type := 1);
procedure Insert_Space(Container: in out Vector;
      Before: in Cursor;
      Position: out Cursor;
      Count: in Count_Type := 1);
These insert the number of empty elements given by Count at the place indicated. Existing elements are slid along as necessary. These should not be confused with the versions of Insert which do not provide an explicit value for the elements – in those cases the new elements take their default values.
Care needs to be taken if we use empty elements. For example we should not compare two vectors using "=" if they have empty elements because this implies reading them. But the big advantage of empty elements is that they provide a quick way to make a large lump of space in a vector which can then be filled in with appropriate values. One big slide is a lot faster than lots of little ones.
For completeness, we briefly mention the remaining few subprograms that are unique to the vectors package.
There are further versions of Insert thus 
procedure Insert(
      Container: in out Vector;
      Before: in Extended_Index; New_Item: in Vector);
procedure Insert(
      Container: in out Vector;
      Before: in Cursor; New_Item: in Vector);
procedure Insert(
      Container: in out Vector;
      Before: in Cursor; New_Item: in Vector; Position: out Cursor);
These insert copies of a vector into another vector (rather than just single elements).
There are also corresponding versions of Prepend and Append thus 
procedure Prepend(Container: in out Vector; New_Item: in Vector);
procedure Append(Container: in out Vector; New_Item: in Vector);
Finally, there are four functions "&" which concatenate vectors and elements by analogy with those for the type String. Their specifications are 
function "&" (Left, Right: Vector) return Vector;
function "&" (Left: Vector; Right: Element_Type) return Vector;
function "&" (Left: Element_Type; Right: Vector) return Vector;
function "&" (Left, Right: Element_Type) return Vector;
Note the similarity between 
Append(V1, V2);
V1 := V1 & V2;
The result is the same but using "&" is less efficient because of the extra copying involved. But "&" is a familiar operation and so is provided for convenience.

Contents   Index   References   Search   Previous   Next 
© 2005, 2006, 2007 John Barnes Informatics.
Sponsored in part by:
The Ada Resource Association and its member companies: ARA Members AdaCore Polyspace Technologies Praxis Critical Systems IBM Rational Sofcheck and   Ada-Europe:
Ada-Europe