Rationale for Ada 2005

John Barnes
Contents   Index   References   Search   Previous   Next 

8.5 Indefinite containers

There are versions of the six container packages we have just been discussing for indefinite types.
As mentioned in Section 8.1, an indefinite (sub)type is one for which we cannot declare an object without giving a constraint (either explicitly or though an initial value). Moreover we cannot have an array of an indefinite subtype. The type String is a good example. Thus we cannot declare an array of the type String because the components might not all be the same size and indexing would be a pain. Class wide types are also indefinite.
The specification of the indefinite container for lists starts 
generic
   type Element_Type(<>) is private;
   with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Indefinite_Doubly_Linked_Lists is
   pragma Preelaborate(Indefinite_Doubly_Linked_Lists);
where we see that the formal type Element_Type has unknown discriminants and so permits the actual type to be any indefinite type (and indeed a definite type as well). So if we want to manipulate lists of strings where the individual strings can be of any length then we declare 
package String_Lists is new  Ada.Containers.Indefinite_Doubly_Linked_Lists(String);
In the case of ordered maps we have 
generic
   type Key_Type(<>) is private;
   type Element_Type(<>)  is private;
   with function "<" (Left, Right: Key_Type) return Boolean is <>;
   with function "=" (Left, Right: Element_Type) return Boolean is <>;
package Ada.Containers.Indefinite_Ordered_Maps is
   pragma Preelaborate(Indefinite_Ordered_Maps);
showing that both Element_Type and Key_Type can be indefinite.
There are two other differences from the definite versions which should be noted.
One is that the Insert procedures for Vectors, Lists and Maps which insert an element with its default value are omitted (because there is no way to create a default initialized object of an indefinite type anyway).
The other is that the parameter Element of the access procedure Process of Update_Element (or the garrulous Update_Element_Preserving_Key in the case of sets) can be constrained even if the type Element_Type is unconstrained.
As an example of the use of an indefinite container consider the problem of creating an index. For each word in a text file we need a list of its occurrences. The individual words can be represented as just objects of the type String. It is perhaps convenient to consider strings to be the same irrespective of the case of characters and so we define 
function Same_Strings(S, T: String) return Boolean is
begin
   return To_Lower(S) = To_Lower(T);
end Same_Strings;
where the function To_Lower is from the package Ada.Characters.Handling.
We can suppose that the positions of the words are described by a type Place thus 
type Place is
   record
      Page: Text_IO.Positive_Count;
      Line: Text_IO.Positive_Count;
      Col: Text_IO.Positive_Count;
   end record;
The index is essentially a map from the type String to a list of values of type Place. We first create a definite list container for handling the lists thus 
package Places is new Doubly_Linked_Lists(Place);
We then create an indefinite map container from the type String to the type List thus 
package Indexes is new Indefinite_Hashed_Maps(
        Key_Type => String;
        Element_Type => Places.List;
        Hash => Ada.Strings.Hash;
        Equivalent_Keys => Same_Strings;
        "=" => Places."=");
The index is then declared by writing 
The_Index: Indexes.Map;
Note that this example illustrates the use of nested containers since the elements in the map are themselves containers (lists).
It might be helpful for the index to contain information saying which file it refers to. We can extend the type Map thus (remember that container types are tagged) 
type Text_Map is new Indexes.Map with
   record
      File_Ref: Text_IO.File_Access;
   end record;
and now we can more usefully declare 
My_Index: Text_Map := (Indexes.Empty_Map with My_File'Access);
We can now declare various subprograms to manipulate our map. For example to add a new item we have first to see whether the word is already in the index – if it is not then we add the new word to the map and set its list to a single element whereas if it is already in the index then we add the new place entry to the corresponding list. Thus 
procedure Add_Entry(Index: in out Text_Map; Word: String; P: Place) is
   M_Cursor: Indexes.Cursor;
   A_List: Places.List;    -- empty list of places
begin
   M_Cursor := Index.Find(Word);
   if M_Cursor = Indexes.No_Element then
      -- it's a new word
      A_List.Append(P);
      Index.Insert(Word, A_List);
   else
      -- it's an old word
      A_List := Element(M_Cursor);    -- get old list
      A_List.Append(P);    -- add to it
      Index.Replace_Element(M_Cursor, A_List);
   end if;
end Add_Entry;
A number of points should be observed. The type Text_Map being derived from Indexes.Map inherits all the map operations and so we can write Index.Find(Word) which uses the prefixed notation (or we can write Indexes.Find(Index, Word)). On the other hand auxiliary entities such as the type Cursor and the constant No_Element are of course in the package Indexes and have to be referred to as Indexes.Cursor and so on.
A big problem with the procedure as written however is that it uses Element and Replace_Element rather than Update_Element. This means that it copies the whole of the existing list, adds the new item to it, and then copies it back. Here is an alternative version 
procedure Add_Entry(Index: in out Text_Map; Word: String; P: Place) is
   M_Cursor: Indexes.Cursor;
   A_List: Places.List;    -- empty list of places
begin
   M_Cursor := Index.Find(Word);
   if M_Cursor = Indexes.No_Element then
      -- it's a new word
      A_List.Append(P);
      Index.Insert(Word, A_List);
   else
      -- it's an old word
      declare
         -- this procedure adds to the list in situ
         procedure Add_It(The_Key: in String; The_List: in out Places.List) is
         begin
            The_List.Append(P);
         end Add_It;
      begin
         -- and here we call it via Update_Element
         Index.Update_Element(M_Cursor, Add_It'Access);
      end;
   end if;
end Add_Entry;
This is still somewhat untidy. In the case of a new word we might as well make the new map entry with an empty list and then update it thereby sharing the calls of Append. We get 
procedure Add_Entry(Index: in out Text_Map; Word: String; P: Place) is
   M_Cursor: Indexes.Cursor := Index.Find(Word);
   OK: Boolean;
begin
   if M_Cursor = Indexes.No_Element then
      -- it's a new word
      Index.Insert(Word, Places.Empty_List, M_Cursor, OK);
      -- M_Cursor now refers to new position
      -- and OK will be True
   end if;
   declare
     -- this procedure adds to the list in situ
      procedure Add_It(The_Key: in String; The_List: in out Places.List) is
      begin
         The_List.Append(P);
      end Add_It;
   begin
      -- and here we call it via Update_Element
      Index.Update_Element(M_Cursor, Add_It'Access);
   end;
end Add_Entry;
It will be recalled that there are various versions of Insert. We have used that which has two out parameters being the position where the node was inserted and a Boolean parameter indicating whether a new node was inserted or not. In this case we know that it will be inserted and so the final parameter is a nuisance (but sadly we cannot default out parameters). Note also that we need not give the parameter Places.Empty_List because another version of Insert will do that automatically since that is the default value of a list anyway.
Yet another approach is not to use Find but just call Insert. We can even use the defaulted version – if the word is present then the node is not changed and the position parameter indicates where it is, if the word is not present then a new node is made with an empty list and again the position parameter indicates where it is.
procedure Add_Entry(Index: in out Text_Map; Word: String; P: Place) is
   M_Cursor: Indexes.Cursor;
   Inserted: Boolean;
begin
   Index.Insert(Word, M_Cursor, Inserted);
   -- M_Cursor now refers to position of node
   -- and Inserted indicates whether it was added
   declare
     -- this procedure adds to the list in situ
      procedure Add_It(The_Key: in String; The_List: in out Places.List) is
      begin
         The_List.Append(P);
      end Add_It;
   begin
      -- and here we call it via Update_Element
      Index.Update_Element(M_Cursor, Add_It'Access);
   end;
end Add_Entry;
Curiously enough we do not need to use the value of Inserted. We leave the reader to decide which of the various approaches is best.
We can now do some queries on the index. For example we might want to know how many different four-lettered words there are in the text. We can either use Iterate or do it ourselves with Next as follows 
function Four_Letters(Index: Text_Map) return Integer is
   Count: Integer := 0;
   C: Indexes.Cursor := Index.First;
begin
   loop
      if Key(C)'Length = 4 then
         Count := Count + 1;
      end if;
      Indexes.Next(C);
      exit when C = Indexes.No_Element;
   end loop;
   return Count;
end Four_Letters;
We might finally wish to know how many four-lettered words there are on a particular page. (This is just an exercise – it would clearly be simplest to search the original text!) We use Iterate this time both to scan the map for the words and then to scan each list for the page number 
function Four_Letters_On_Page(
         Index: Text_Map;
         Page: Text_IO.Positive_Count) return Integer is
   Count: Integer := 0;
   procedure Do_It_Map(C: Indexes.Cursor) is
      procedure Do_It_List(C: Places.Cursor) is
      begin
         if Element(C).Page = Page then
            Count := Count + 1;
         end if;
      end Do_It_LIst;
      procedure Action(K: String; E: Places.List) is
      begin
         if K'Length = 4 then
            -- now scan list for instances of Page
            E.Iterate(Do_It_List'Access);
         end if;
      end Action;
   begin
      Indexes.Query_Element(C, Action'Access);
   end Do_It_Map;
begin
   Index.Iterate(Do_It_Map'Access);
   return Count;
end Four_Letters_On_Page;
We could of course have used First and Next to search the list. But in any event the important point is that by using Query_Element we do not have to copy the list in order to scan it.

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