Ada Programming/Containers


What follows is a simple demo of some of the container types. It does not cover everything, but should get you started.

Ada. Time-tested, safe and secure.
Ada. Time-tested, safe and secure.

This language feature is only available from Ada 2005 on.

First Example: Maps edit

The program below prints greetings to the world in a number of human languages. The greetings are stored in a table, or hashed map. The map associates every greeting (a value) with a language code (a key). That is, you can use language codes as keys to find greeting values in the table.

The elements in the map are constant strings of international characters, or really, pointers to such constant strings. A package Regional is used to set up both the language IDs and an instance of Ada.Containers.Hashed_Maps.

File: regional.ads (view, plain text, download page, browse all)
with Ada.Containers.Hashed_Maps;  use Ada.Containers;

package Regional is

   type Language_ID is (DE, EL, EN, ES, FR, NL);
   --  a selection from the two-letter codes for human languages

   type Hello_Text is access constant Wide_String;
   --  objects will contain a «hello»-string in some language


   function ID_Hashed (id: Language_ID) return Hash_Type;
   --  you need to provide this to every hashed container

   package Phrases is new Ada.Containers.Hashed_Maps
     (Key_Type => Language_ID,
      Element_Type => Hello_Text,
      Hash => ID_Hashed,
      Equivalent_Keys => "=");

end Regional;

Here is the program, details will be explained later.

File: hello_world_extended.ads (view, plain text, download page, browse all)
with Regional; use Regional;
with Ada.Wide_Text_IO; use Ada;

procedure Hello_World_Extended is

   --  print greetings in different spoken languages

   greetings: Phrases.Map;
   --  the dictionary of greetings

begin -- Hello_World_Extended

   Phrases.Insert(greetings,
                  Key => EN,
                  New_Item => new Wide_String'("Hello, World!"));

   --  or, shorter,
   greetings.Insert(DE, new Wide_String'("Hallo, Welt!"));
   greetings.Insert(NL, new Wide_String'("Hallo, Wereld!"));
   greetings.Insert(ES, new Wide_String'("¡Hola mundo!")); 
   greetings.Insert(FR, new Wide_String'("Bonjour, Monde!"));
   greetings.Insert(EL, new Wide_String'("Γεια σου κόσμε"));

   declare
      use Phrases;

      speaker: Cursor := First(greetings);
   begin
      while Has_Element(speaker) loop
         Wide_Text_IO.Put_Line( Element(speaker).all );
         Next(speaker);
      end loop;
   end;

end Hello_World_Extended;

The first of the Insert statements is written in an Ada 95 style:

  Phrases.Insert(greetings,
                 Key => EN,
                 New_Item => new Wide_String'("Hello, World!"));

The next insertions use so called distinguished receiver notation which you can use in Ada 2005. (It's O-O parlance. While the Insert call involves all of: a Container object (greetings), a Key object (EN), and a New_Item object (new Wide_String'("Hello, World!")), the Container object is distinguished from the others in that the Insert call provides it (and only it) with the other objects. In this case the Container object will be modified by the call, using arguments named Key and New_Item for the modification.)

  greetings.Insert(ES, new Wide_String'("¡Hola mundo!"));

After the table is set up, the program goes on to print all the greetings contained in the table. It does so employing a cursor that runs along the elements in the table in some order. The typical scheme is to obtain a cursor, here using First, and then to iterate the following calls:

  1. Has_Element, for checking whether the cursor is at an element
  2. Element, to get the element and
  3. Next, to move the cursor to another element

When there is no more element left, the cursor will have the special value No_Element. Actually, this is an iteration scheme that can be used with all containers in child packages of Ada.Containers.

A slight variation: picking an element edit

The next program shows how to pick a value from the map, given a key. Actually, you will provide the key. The program is like the previous one, except that it doesn't just print all the elements in the map, but picks one based on a Language_ID value that it reads from standard input.

File: hello_world_pick.adb (view, plain text, download page, browse all)
with Regional; use Regional;
with Ada.Wide_Text_IO; use Ada;

procedure Hello_World_Pick is

  ... as before ...

 declare
     use Phrases;
 
     package Lang_IO is new Wide_Text_IO.Enumeration_IO(Language_ID);
     lang: Language_ID;
  begin
     Lang_IO.Get(lang);
     Wide_Text_IO.Put_Line( greetings.Element(lang).all );
  end;
 
end Hello_World_Pick;

This time the Element function consumes a Key (lang) not a Cursor. Actually, it consumes two values, the other value being greetings, in distinguished receiver notation.

Second Example: Vectors and Maps edit

Let's take bean counting literally. Red beans, green beans, and white beans. (Yes, white beans really do exist.) Your job will be to collect a number of beans, weigh them, and then determine the average weight of red, green, and white beans, respectively. Here is one approach.

Again, we need a package, this time for storing vegetable related information. Introducing the Beans package (the Grams type doesn't belong in a vegetable package, but it's there to keep things simple):

File: 1/beans.ads (view, plain text, download page, browse all)
with Ada.Containers.Vectors;

package Beans is

   type Bean_Color is (R, G, W);
   --  red, green, and white beans

   type Grams is delta 0.01 digits 7;
   --  enough to weigh things as light as beans but also as heavy as
   --  many of them

   type Bean is
   --  info about a single bean
      record
         kind: Bean_Color;
         weight: Grams;
      end record;

   subtype Bean_Count is Positive range 1 .. 1_000;
   --  numbers of beans to count (how many does Cinderella have to count?)

   package Bean_Vecs is new Ada.Containers.Vectors
     (Element_Type => Bean,
      Index_Type => Bean_Count);

end Beans;

The Vectors instance offers a data structure similar to an array that can change its size at run time. It is called Vector. Each bean that is read will be appended to a Bean_Vecs.Vector object.

The following program first calls read_input to fill a buffer with beans. Next, it calls a function that computes the average weight of beans having the same color. This function:

with Beans;   use Beans;

function average_weight
  (buffer: Bean_Vecs.Vector; desired_color: Bean_Color) return Grams;
--  scan `buffer` for all beans that have `desired_color`. Compute the
--  mean of their `.weight` components


Then the average value is printed for beans of each color and the program stops.

File: 1/bean_counting.adb (view, plain text, download page, browse all)
with Beans;
with average_weight;
with Ada.Wide_Text_IO;

procedure bean_counting is
   use Beans, Ada;

   buffer: Bean_Vecs.Vector;

   procedure read_input(buf: in out Bean_Vecs.Vector) is separate;
   --  collect information from a series of bean measurements into `buf`


begin --  bean_counting

   read_input(buffer);

   --  now everything is set up for computing some statistical data.
   --  For every bean color in `Bean_Color`, the function `average_weight`
   --  will scan `buffer` once, and accumulate statistical data from
   --  each element encountered.

   for kind in Bean_Color loop
      Wide_Text_IO.Put_Line
        (Bean_Color'Wide_Image(kind) &
         " ø =" & Grams'Wide_Image( average_weight(buffer, kind) ));
   end loop;

end bean_counting;

All container operations take place in function average_weight. To find the mean weight of beans of the same color, the function is looking at all beans in order. If a bean has the right color, average_weight adds its weight to the total weight, and increases the number of beans counted by 1.

The computation visits all beans. The iteration that is necessary for going from one bean to the next and then performing the above steps is best left to the Iterate procedure which is part of all container packages. To do so, wrap the above steps inside some procedure and pass this procedure to Iterate. The effect is that Iterate calls your procedure for each element in the vector, passing a cursor value to your procedure, one for each element.

Having the container machinery do the iteration can also be faster than moving and checking the cursor yourself, as was done in the Hello_World_Extended example.

File: average_weight.adb (view, plain text, download page, browse all)
with Beans;  use Beans.Bean_Vecs;

function average_weight
  (buffer: Bean_Vecs.Vector; desired_color: Bean_Color) return Grams
is
   total: Grams := 0.0;
   --  weight of all beans in `buffer` having `desired_color`

   number: Natural := 0;
   --  number of beans in `buffer` having `desired_color`

   procedure accumulate(c: Cursor) is
      --  if the element at `c` has the `desired_color`, measure it
   begin
      if Element(c).kind = desired_color then
         number := number + 1;
         total := total + Element(c).weight;
      end if;
   end accumulate;

begin --  average_weight

   Iterate(buffer, accumulate'Access);

   if number > 0 then
      return total / number;
   else
      return 0.0;
   end if;

end average_weight;

This approach is straightforward. However, imagine larger vectors. average_weight will visit all elements repeatedly for each color. If there are M colors and N beans, average_weight will be called M * N times, and with each new color, N more calls are necessary. A possible alternative is to collect all information about a bean once it is visited. However, this will likely need more variables, and you will have to find a way to return more than one result (one average for each color), etc. Try it!

A different approach might be better. One is to copy beans of different colors to separate vector objects. (Remembering Cinderella.) Then average_weight must visit each element only one time. The following procedure does this, using a new type from Beans, called Bean_Pots.

   ...
   type Bean_Pots is array(Bean_Color) of Bean_Vecs.Vector;
   ...

Note how this plain array associates colors with Vectors. The procedure for getting the beans into the right bowls uses the bean color as array index for finding the right bowl (vector).

File: 2/gather_into_pots.adb (view, plain text, download page, browse all)
procedure gather_into_pots(buffer: Bean_Vecs.Vector; pots: in out Bean_Pots) is
   use Bean_Vecs;

   procedure put_into_right_pot(c: Cursor) is
      --  select the proper bowl for the bean at `c` and «append»
      --  the bean to the selected bowl
   begin
      Append(pots(Element(c).kind), Element(c));
   end put_into_right_pot;

begin  --  gather_into_pots
   Iterate(buffer, put_into_right_pot'Access);
end gather_into_pots;


Everything is in place now.

File: 2/bean_counting.adb (view, plain text, download page, browse all)
with Beans;
with average_weight;
with gather_into_pots;
with Ada.Wide_Text_IO;

procedure bean_counting is
   use Beans, Ada;

   buffer: Bean_Vecs.Vector;
   bowls: Bean_Pots;

   procedure read_input(buf: in out Bean_Vecs.Vector) is separate;
   --  collect information from a series of bean measurements into `buf`


begin --  bean_counting

   read_input(buffer);

   --  now everything is set up for computing some statistical data.
   --  Gather the beans into the right pot by color.
   --  Then find the average weight of beans in each pot.

   gather_into_pots(buffer, bowls);

   for color in Bean_Color loop
      Wide_Text_IO.Put_Line
        (Bean_Color'Wide_Image(color)
         & " ø ="
         & Grams'Wide_Image(average_weight(bowls(color), color)));
   end loop;

end bean_counting;


As a side effect of having chosen one vector per color, we can determine the number of beans in each vector by calling the Length function. But average_weight, too, computes the number of elements in the vector. Hence, a summing function might replace average_weight here.

All In Just One Map! edit

The following program first calls read_input to fill a buffer with beans. Then, information about these beans is stored in a table, mapping bean properties to numbers of occurrence. The processing that starts at Iterate uses chained procedure calls typical of the Ada.Containers iteration mechanism.

The Beans package in this example instantiates another generic library unit, Ada.Containers.Ordered_Maps. Where the Ada.Containers.Hashed_Maps require a hashing function, Ada.Containers.Ordered_Maps require a comparison function. We provide one, "<", which sorts beans first by color, then by weight. It will automatically be associated with the corresponding generic formal function, as its name, "<", matches that of the generic formal function, "<".

   ...
   function "<"(a, b: Bean) return Boolean;
   --  order beans, first by color, then by weight

   package Bean_Statistics
     --  instances will map beans of a particular color and weight to the
     --  number of times they have been inserted.
   is new Ada.Containers.Ordered_Maps
     (Element_Type => Natural,
      Key_Type => Bean);
   ...


Where the previous examples have withed subprograms, this variation on bean_counting packs them all as local subprograms.

File: 3/bean_counting.adb (view, plain text, download page, browse all)
with Beans;
with Ada.Wide_Text_IO;

procedure bean_counting is
   use Beans, Ada;

   buffer: Bean_Vecs.Vector;
   stats_cw: Bean_Statistics.Map;
   --  maps beans to numbers of occurrences, grouped by color, ordered by
   --  weight

   procedure read_input(buf: in out Bean_Vecs.Vector) is separate;
   --  collect information from a series of bean measurements into `buf`

   procedure add_bean_info(specimen: in Bean);
   --  insert bean `specimen` as a key into the `stats_cw` table unless
   --  present. In any case, increase the count associated with this key
   --  by 1. That is, count the number of equal beans.

   procedure add_bean_info(specimen: in Bean) is

      procedure one_more(b: in Bean; n: in out Natural) is
       --   increase the count associated with this kind of bean
      begin
         n := n + 1;
      end one_more;

      c : Bean_Statistics.Cursor;
      inserted: Boolean;
   begin
      stats_cw.Insert(specimen, 0, c, inserted);
      Bean_Statistics.Update_Element(c, one_more'Access);
   end add_bean_info;

begin --  bean_counting

   read_input(buffer);

   --  next, for all beans in the vector `buffer` just filled, store
   --  information about each bean in the `stats_cw` table.

   declare
      use Bean_Vecs;

      procedure count_bean(c: Cursor) is
      begin
         add_bean_info(Element(c));
      end count_bean;
   begin
      Iterate(buffer, count_bean'Access);
   end;

   --  now everything is set up for computing some statistical data. The
   --  keys of the map, i.e. beans, are ordered by color and then weight.
   --  The `First`, and `Ceiling` functions will find cursors
   --  denoting the ends of a group.


   declare
      use Bean_Statistics;

      --  statistics is computed groupwise:

      q_sum: Grams;
      q_count: Natural;

      procedure q_stats(lo, hi: Cursor);
      --  `q_stats` will update the `q_sum` and `q_count` globals with
      --  the sum of the key weights and their number, respectively. `lo`
      --  (included) and `hi` (excluded)  mark the interval of keys
      --  to use from the map.

      procedure q_stats(lo, hi: Cursor) is
         k: Cursor := lo;
      begin
         q_count := 0; q_sum := 0.0;
         loop
            exit when k = hi;
            q_count := q_count + Element(k);
            q_sum := q_sum + Key(k).weight * Element(k);
            Next(k);
         end loop;
      end q_stats;


      --  precondition
      pragma assert(not Is_Empty(stats_cw), "container is empty");

      lower, upper: Cursor := First(stats_cw);
      --  denoting the first key of a group, and the first key of a
      --  following group, respectively

   begin

      --  start reporting and trigger the computations

      Wide_Text_IO.Put_Line("Summary:");

      for color in Bean_Color loop
         lower := upper;
         if color = Bean_Color'Last then
            upper := No_Element;
         else
            upper := Ceiling(stats_cw, Bean'(Bean_Color'Succ(color), 0.0));
         end if;

         q_stats(lower, upper);

         if q_count > 0 then
            Wide_Text_IO.Put_Line
              (Bean_Color'Wide_Image(color) & " group:" &
               "  ø =" & Grams'Wide_Image(q_sum / q_count) &
               ", # =" & Natural'Wide_Image(q_count) &
               ", Σ =" & Grams'Wide_Image(q_sum));
         end if;
      end loop;
   end;

end bean_counting;

Like in the greetings example, you can pick values from the table. This time the values tell the number of occurrences of beans with certain properties. The stats_cw table is ordered by key, that is by bean properties. Given particular properties, you can use the Floor and Ceiling functions to approximate the bean in the table that most closely matches the desired properties.

It is now easy to print a histogram showing the frequency with which each kind of bean has occurred. If N is the number of beans of a kind, then print N characters on a line, or draw a graphical bar of length N, etc. A histogram showing the number of beans per color can be drawn after computing the sum of beans of this color, using groups like in the previous example. You can delete beans of a color from the table using the same technique.

Finally, think of marshalling the beans in order starting at the least frequently occurring kind. That is, construct a vector appending first beans that have occurred just once, followed by beans that have occurred twice, if any, and so on. Starting from the table is possible, but be sure to have a look at the sorting functions of Ada.Containers.

See also edit

Wikibook edit

Ada 2005 Reference Manual edit