Programming Ada: Records And Containers For Organized Code

Writing code without having some way to easily organize sets of variables or data would be a real bother. Even if in the end you could totally do all of the shuffling of bits and allocating in memory by yourself, it’s much easier when the programming language abstracts all of that housekeeping away. In Ada you generally use a few standard types, ranging from records (equivalent to structs in C) to a series of containers like vectors and maps. As with any language, there are some subtle details about how all of these work, which is where the usage of these types in the Sarge project will act as an illustrative example.

In this project’s Ada code, a record is used for information about command line arguments (flag names, values, etc.) with these argument records stored in a vector. In addition, a map is created that links the names of these arguments, using a string as the key, to the index of the corresponding record in the vector. Finally, a second vector is used to store any text fragments that follow the list of arguments provided on the command line. This then provides a number of ways to access the record information, either sequentially in the arguments vector, or by argument (flag) name via the map.

Introducing Generics

Not unlike the containers provided by the Standard Template Library (STL) of C++, the containers provided by Ada are provided as generics, meaning that they cannot be used directly. Instead we have to create a new package that uses the container generic to formulate a container implementation limited to the types which we intend to use with it. For a start let’s take a look at how to create a vector:

with Ada.Containers.Vectors;
use Ada.Containers;
package arg_vector is new Vectors(Natural, Argument);

The standard containers are part of the Ada.Containers package, which we include here before the instantiating of the desired arguments vector, which is indexed using natural numbers (all positive integers, no zero or negative numbers), and with the Argument type as value. This latter type is the custom record, which is defined as follows:

type Argument is record
    arg_short: aliased Unbounded_String;
    arg_long: aliased Unbounded_String;
    description: aliased Unbounded_String;
    hasValue: aliased boolean := False;
    value: aliased Unbounded_String;
    parsed: aliased boolean := False;
end record;

Here the aliased keyword means that the variable will have a memory address rather than only exist in a register. This is a self-optimizing feature of Ada that is being copied by languages like C and C++ that used to require the inverse action by the programmer in the form of the C & C++ register keyword. For Ada’s aliased keyword, this means that the variable it is associated with can have its access (‘pointer’, in C parlance) taken.

Moving on, we can now create the two vectors and the one map, starting with the arguments vector using the earlier defined arg_vector package:

args : arg_vector.vector;

The text arguments vector is created effectively the same way, just with an unbounded string as its value:

package tArgVector is new Vectors(Natural, Unbounded_String);
textArguments: tArgVector.vector;

Finally, the map container is created in a similar fashion. Note that for this we are using the Ada.Containers.Indefinite_Ordered_Maps package. Ordered maps contrast with hashed maps in that they do not require a hash function, but will use the < operator (existing for the type or custom).  These maps provide a look-up time defined as O(log N), which is faster than the O(N) of a vector and the reason why the map is used as an index for the vector here.

package argNames_map is new Indefinite_Ordered_Maps(Unbounded_String, Natural);
argNames: argNames_map.map;

With these packages and instances defined and instantiated, we are now ready to fill them with data.

Cross Mapping

When we define a new argument to look for when parsing command line arguments, we have to perform three operations: first create a new Argument record instance and assign its members the relevant information, secondly we assign this record to the args vector. The record is provided with data via the setArgument procedure:

procedure setArgument(arg_short: in Unbounded_String; arg_long: in Unbounded_String; 
                            desc: in Unbounded_String; hasVal: in boolean);

This allows us to create the Argument instance as follows in the initialization section (before begin in the procedure block) as follows:

arg: aliased Argument := (arg_short => arg_short, arg_long => arg_long, 
                          description => desc, hasValue => hasVal, 
                          value => +"", parsed => False);

This Argument record can then be added to the args vector:

args.append(arg);

Next we have to set up links between the flag names (short and long version) in the map to the relevant index in the argument vector:

argNames.include(arg_short, args.Last_Index);
argNames.include(arg_long, args.Last_Index);

This sets the key for the map entry to the short or long version of the flag, and takes the last added (highest) index of the arguments vector for the value. We’re now ready to find and update records.

Search And Insert

Using the contraption which we just setup is fairly straightforward. If we want to check for example that an argument flag has been defined or not, we can use the arguments vector and the map as follows:

flag_it: argNames_map.Cursor;
flag_it := argNames.find(arg_flag);
if flag_it = argNames_map.No_Element then
    return False;
elsif args(argNames_map.Element(flag_it)).parsed /= True then
    return False;
end if;

This same method can be used to find a specific record to update the freshly parsed value that we expect to trail certain flags:

flag_it: argNames_map.Cursor;
flag_it := argNames.find(arg_flag);
args.Reference(argNames_map.Element(flag_it)).value := arg;

Using the reference function on the args vector gets us a reference to the element which we can then update, unlike the element function of the package.

We can now easily check that a particular flag has been found by looking up its record in the vector and return the found value, as defined in the getFlag function in the sarge.adb file of Sarge:

function getFlag(arg_flag: in Unbounded_String; arg_value: out Unbounded_String) return boolean is
flag_it: argNames_map.Cursor;
use argNames_map;
begin
    if parsed /= True then
        return False;
    end if;

    flag_it := argNames.find(arg_flag);
    if flag_it = argNames_map.No_Element then
         return False;
    elsif args(argNames_map.Element(flag_it)).parsed /= True then
        return False;
    end if;

    if args(argNames_map.Element(flag_it)).hasValue = True then
        arg_value := args(argNames_map.Element(flag_it)).value;
    end if;

    return True;
end getFlag;

Other Containers

There are of course many more containers than just the two types covered here defined in Ada’s Predefined Language Library (PLL). For instance, sets are effectively like vectors, except that they only allow for unique elements to exist within the container. This is only the beginning of the available containers, though, with the Ada 2005 standard defining only the first collection, which got massively extended in the Ada 2012 standard (which we focus on here). These include trees, queues, linked lists and so on. We’ll cover some of these in more detail in upcoming articles.

Together with the packages, functions and procedures covered earlier in this series, records and containers form the basics of organizing code in Ada. Naturally, Ada also supports more advanced types of modularization and reusability, such as object-oriented programming, which will also be covered in upcoming articles.

5 thoughts on “Programming Ada: Records And Containers For Organized Code

Leave a Reply

Please be kind and respectful to help make the comments section excellent. (Comment Policy)

This site uses Akismet to reduce spam. Learn how your comment data is processed.