Previous

Contents

Next

Chapter 6:
Composite data types

Yea, from the table of my memory
I’ll wipe away all trivial fond records.

— William Shakespeare, Hamlet


6.1 Record types
6.2 Strings
6.3 Declaring array types
6.4 Unconstrained types
6.5 For loops revisited
6.6 A simple sorting procedure
6.7 Multidimensional arrays
6.8 Discriminants
6.9 Limited types
6.10 Using packages with data types
Exercises

6.1 Record types

In the last chapter you saw how Ada allows you to define data types which can be tailored fairly closely to the type of information that a particular program is concerned with modelling. Numeric types of various sorts can be defined to represent different types of numerical information; enumerated types can be used when a set of non-numerical values is needed; strings can be used when the information is textual in nature. Each of these data types that you define comes complete with a set of operations that you can perform on them, as for example the arithmetic operations provided for numeric types.

All the types described in the last chapter are used to represent individual values; they are known collectively as scalar types (see Appendix A for a complete list of the hierarchy of types in Ada). However, in most real-life situations the data you deal with can’t be represented by simple numbers or enumerations of possible values. Most data is composite in nature, consisting of a collection of simpler data items. For example, a date consists of a day, a month and a year; a time consists of an hour and a minute. The data types we’ve used so far are adequate for representing each of these individual components; days, years, hours and minutes are all numerical and months are either enumerations or numbers. For example, here are some possible declarations for types to represent days, months and years:

    subtype Day_Type is Integer range 1..31;
    type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
    subtype Year_Type is Integer range 1901..2099;

Although you could represent a date using three separate variables (day, month and year), this is not a very satisfactory solution. Every procedure or function that dealt with a date would require three parameters instead of one. Functions would not be able to return date results, since a function can only return a single result but a date consists of three separate values. Worse, it wouldn’t necessarily be obvious that the three variables were parts of a common whole. You could end up supplying a procedure with the day from one date and the month from another by mistake as the result of a simple typing error. All this can lead to some pretty horrible debugging and maintenance problems.

The solution in Ada is to gather the components of the data type together into a single type known as a record type. Here is how we could define a record type to represent a date:

    type Date_Type is
        record
            Day   : Day_Type;
            Month : Month_Type;
            Year  : Year_Type;
        end record;

Given this type declaration, variables of type Date_Type can be declared just like variables of any other data type:

    Now, Later : Date_Type;

Notice that the individual components of the type defined between record and end record look just like variable declarations. You can if you like think of Date_Type as a type which acts as a container for three separate objects called Day, Month and Year, so that the variables Later and Now each contain three distinct subvariables, normally referred to as components (or sometimes fields). The components of a record type can be selected by using the same ‘dot’ notation you’re already familiar with for selecting procedures from within a package, so that the Day component of Later can be referred to as Later.Day. In other words, you can treat Later.Day as a variable of type Day_Type, or Now.Month as a variable of type Month_Type. To copy one date into another you could do something like this:

    Later.Day   := Now.Day;
    Later.Month := Now.Month;
    Later.Year  := Now.Year;

However, a variable of type Date_Type can also be treated as a single item, so that a Date_Type value can be passed as a parameter to a procedure or returned from a function. You can also assign one Date_Type variable to another in a single operation, so that the three assignment statements above can be written like this:

    Later := Now;

Although the effect is exactly the same, it’s much simpler and clearer than using three separate assignments as well as reducing the risk of error. The more statements you have to write to do something, the more chance there is of making a mistake. Also, if you have to change this when you maintain the program, you only have to change one statement instead of three; the more changes you have to make, the greater the risk of errors creeping in. The only other standard operations on record types are tests for equality and inequality:

    if Later = Now then ...
    if Later /= Now then ...

You can also use an aggregate to build a record from a set of components:

    Later := (25,Dec,1995);
    if Now = (25,Dec,1995) then ...
    if Now /= (25,Dec,1995) then ...

An aggregate is simply a list of values of the appropriate types enclosed in parentheses. The assignment above is equivalent to the following longer-winded set of three assignment statements:

    Later.Day   := 25;
    Later.Month := Dec;
    Later.Year  := 1995;

You can also use the names of the components in an aggregate:

    Later := (Day=>25, Month=>Dec, Year=>1995);        -- same as above

If you do this you can write the values for the components in any order since the compiler can use the component names to arrange them into the correct order:

    Later := (Month=>Dec, Year=>1995, Day=>25);        -- same as above

Aggregates are often used to provide an initial value for a variable as part of a declaration; they can also be used for declaring constants:

    Birthday  : Date_Type          := (25,Jan,1956);
    Christmas : constant Date_Type := (25,Dec,1995);

Record types are types just like any other. You can use them in exactly the same way as any other type; they can be used for parameters to procedures, function results, even components of other records. If we declare a record type to represent a time of day like this:

    subtype Hour_Type is Integer range 0..23;
    subtype Minute_Type is Integer range 0..59;
    type Time_Type is
        record
            Hour   : Hour_Type;
            Minute : Minute_Type;
        end record;

we can define yet another record type which contains a date and a time, perhaps for recording the date and time of an appointment:

    type Appointment_Type is
        record
            Date : Date_Type;
            Time : Time_Type;
        end record;

Given a variable A of type Appointment_Type, its components can be referred to like this:

    A                 -- the date and time as a whole (Appointment_Type)
    A.Date            -- the date of the appointment (Date_Type)
    A.Date.Day        -- the day of the appointment (Day_Type)
    A.Date.Month      -- the month of the appointment (Month_Type)
    A.Date.Year       -- the year of the appointment (Year_Type)
    A.Time            -- the time of the appointment (Time_Type)
    A.Time.Hour       -- the hour of the appointment (Hour_Type)
    A.Time.Minute     -- the minute of the appointment (Minute_Type)

So, A is name of the date-and-time record as a whole. It’s of type Appointment_Type, so we can select its Date component by saying A.Date; this is of type Date_Type, and so we can select the Day component of the date by saying A.Date.Day.


6.2 Strings

Of course, the one thing that’s missing from the Appointment_Type above is a description of the appointment. We could represent this as a string using the standard type String. String is a predefined array type; an array is a collection of items of the same type. This is in contrast to record types, where the components can be different types. In the case of String, the individual components of the array are Characters. I’ll describe how you can define your own array types in the next section, but for now I’ll use String as an example of an array type to show you how arrays can be used.

To declare a string variable you have to specify how many characters it can hold:

    Details : String (1..100);   -- a 100-character string
    Name    : String (1..10);    -- a 10-character string

Another way of doing the same thing would be to declare subtypes to specify the length:

    subtype Details_Type is String (1..100);
    subtype Name_Type is String (1..10);
    Details : Details_Type;
    Name    : Name_Type;

Both methods give you strings called Details and Name; Details can hold 100 characters numbered 1 to 100, while Name can hold ten characters numbered 1 to 10. You can select individual characters from Details using an index between 1 and 100:

    Details(1)   := 'x';
    Details(I+1) := 'x';

The range of possible index values is known as the index subtype of the array. If you try to access the array with an index which isn’t within the range of the index subtype, you’ll get a Constraint_Error. Both of the assignments above set a specified character in Details to x. In the first case it’s the first character; in the second case the character selected depends on the value of the expression I + 1. If the value of I + 1 isn’t within the range of the index subtype (i.e. 1 to 100) you’ll get a Constraint_Error. Note that the individual elements of the string are of type Character.

As well as dealing with individual array elements, you can deal with slices of a string:

    Details(1..5) := "Hello";
    Details(2..6) := Details(1..5);

The first assignment copies the five-character string "Hello" into the first five characters of Details. The second assignment copies the first five characters into the five characters beginning at the second character of the string, so that after these two statements the first six characters of Details will be "HHello". As you can see, it doesn’t matter if the slices overlap; a copy of the slice is taken and then the copy is stored in the destination slice. Note that slices are also arrays of characters, i.e. Strings.

The length of the string that you assign to a slice must be the same as the length of the slice; the same applies to Details as a whole:

    Details := "a 100-character string ";

This will be all right provided that the length of the string being assigned to Details is in fact exactly 100 characters long. Extremely long strings like this might not fit on one line, in which case you can use the concatenation operator ‘&’ to join two shorter strings end to end to create a longer one:

    Details := "a 50-character string " &
               "another 50-character string ";

You could use slicing and the concatenation operator to interchange the two halves of Details (but I’m not sure why you would want to; this is only an example!). You would do it like this:

    Details := Details(51..100) & Details(1..50);

The two slices are the last 50 characters and the first 50 characters of Details. These are then concatenated together to give a single 100-character string which can be assigned to Details.

If you want to fill an entire string with spaces, you can do it like this:

    Name := " ";    -- exactly 10 spaces

The string literal above is actually an abbreviation for the following array aggregate:

    Name := (' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ');
                                    -- an aggregate containing 10 spaces

This can be awkward with longer strings like Details. When all the characters are the same (and especially when there are a lot of them) you can simplify matters by writing the aggregate like this:

    Details := (1..100 => ' ');     -- no need to write exactly 100 spaces!

This specifies that each character with an index in the range 1 to 100 is to be set to a space, and avoids having to write a string literal which contains exactly the right number of characters and to modify it if the string size needs to be changed. Better still, you can avoid mentioning the string bounds at all like this:

    Details := (others => ' ');     -- fill string with spaces

You can use the same notations as in case statements:

    Details := (1..10 => 'x', 11..13 | 21..23 | 27 | 29 => 'y', others => ' ');

This sets each of the first ten characters of Details to x, characters 11 to 13, 21 to 23, 27 and 29 to y (a total of eight characters) and the remaining 82 characters to spaces.

The easiest way to read in a string is to use the procedure Get_Line defined in Ada.Text_IO:

    Get_Line (Details, N);

This will read a line of up to 100 characters (the size of Details) into Details from the keyboard. N is an ‘out Natural’ parameter which is set by Get_Line to the actual number of characters read, so that you can then access the characters that you read in by using the slice Details (1..N).

Arrays can be compared using the comparison operators (=, /=, <, >, <=, >=). In the case of String, two strings are equal if they are the same length and contain the same characters. For comparing using ‘<’ and so on, the characters of the strings are compared from left to right until either the strings are found to differ or the end of the shorter string is reached. This more or less gives alphabetical ordering (but not perfectly; 'a' comes after 'Z' in the definition of Character, and if you use accented characters as in French 'à' comes after 'z'). Here are some examples:

    "abc" = "abc"       "abc" /= "ab"       "abc" /= ""
    "abc" < "abd"       "abc" > "ab"        "abc" > ""

The comparison operations for other array types are defined in the same way. You can only compare arrays if the individual components can be compared, so that you couldn’t use ‘<’ to compare an array of records since you can’t compare records using ‘<’. You could, however, compare two arrays of records for equality since you can compare individual records for equality. The logical operations and, or, not and xor are also defined for arrays provided that the individual components support these operations (e.g. an array of Booleans); the operation will be applied to corresponding components in the two arrays.

Now we can use String to finish off the appointment type defined earlier:

    type Appointment_Type is
        record
            Date    : Date_Type;
            Time    : Time_Type;
            Details : String (1..50);
        end record;

Note that if A is an Appointment_Type value, A.Details is a 50-character string and A.Details(1) is the first character of the string.


6.3 Declaring array types

We can use Appointment_Type to represent appointments in an automated appointments diary program. However, an appointments diary program will need to manage more than one appointment so we’ll need to be able to declare arrays of appointments. Here’s how we could declare an array type to hold 100 appointments:

    type Appt_Array is array (1..100) of Appointment_Type;

This defines Appt_Array as a type describing a collection of appointments which can be individually selected by an index between 1 and 100. Appt_Array variables can be declared in the normal way:

    Appt_List : Appt_Array;            -- a list of 100 appointments

The individual appointments can then be accessed as Appt_List(1), Appt_List(2) and so on up to Appt_List(100), just like selecting an individual character from a string. You can slice any array in exactly the same way as you can slice a string; the first five appointments of Appt_List can be sliced out as Appt_List(1..5). As I mentioned earlier, anything you can do with a string (indexing it to select individual characters, slicing it to get a smaller string) can be done with any other array type; similarly all the new features of arrays I’ll be describing below (array attributes and so on) apply to strings as well.

The first appointment in the array would be referred to as Appt_List(1). Since this is an individual array element of type Appointment_Type, we can refer to the details of the appointment as Appt_List(1).Details. Since this is a string you can then select its first character by referring to Appt_List(1).Details(1).

If you only want one array of a particular type, you can use the array type specification directly in the declaration of a variable:

    Appt_List : array (1..100) of Appointment_Type;

The drawback with doing this is that it is equivalent to:

    type ??? is array (1..100) of Appointment_Type;    -- not Ada!
    Appt_List : ???;         -- "???" represents an anonymous type

You don’t have a name for the type of Appt_List, so you can’t declare any more arrays of the same type. This means you won’t be able to assign one array to another in a single operation (or a slice of one array to another); you won’t be able to use the name of the array type in a procedure parameter declaration so you won’t be able to pass the array (or a slice of it) as a parameter to a procedure. However, you can still use individual array elements since each element has a known type, namely Appointment_Type.

One place where anonymous arrays are handy is in defining lookup tables; for example, the following declares an array containing the number of days in each month:

    Month_Length : constant array (1..12) of Positive :=
                            (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

Another way to declare the array is like this:

    Month_Length : constant array (1..12) of Positive :=
                            (4 | 6 | 9 | 11 => 30, 2 => 28, others => 31);

With this declaration, Month_Length(1) is 31, which tells us that January (month 1) has 31 days. The length of any month N is given by Month_Length (N), although it doesn’t cater for February having 29 days in leap years.

The index subtype for an array type doesn’t have to be a range of integers; it can be any discrete type (i.e. any integer or enumeration type) or a range of values of a discrete type:

    Hours_Worked : array (Day_Of_Week range Mon..Fri) of Natural;

If you just give a range like 1..100 without specifying a particular type, the type Integer is assumed, so that the declaration of Appointment_Array given earlier is equivalent to this:

    type Appt_Array is array (Integer range 1..100) of Appointment_Type;

We could redefine Month_Length to use the enumeration type Month_Type declared earlier:

    Month_Length : constant array (Month_Type) of Positive :=
                            (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

Or equivalently:

    Month_Length : constant array (Month_Type) of Positive :=
                        (Apr | Jun | Sep | Nov => 30, Feb => 28, others => 31);

The individual elements of Month_Length would then be accessed as Month_Length(Jan), Month_Length(Feb) and so on. Here’s another useful one using the type Day_Of_Week from chapter 5:

    Tomorrow : constant array (Day_Of_Week) of Day_Of_Week :=
                            (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

Tomorrow(Sun) is Mon, Tomorrow(Mon) is Tue, and so on up to Tomorrow(Sat) which is Sun. This can be used to get around the problem that Day_Of_Week'Succ can’t be used to get the day after Saturday since Sat is the last value of the type.


6.4 Unconstrained types

It is usually better not to specify the exact size of an array in the type declaration; this ties you down so that all arrays of that type have to have exactly the same number of elements. It’s much better to say that an array type is a collection of unspecified size to give yourself more room for manoeuvre. For example, the definition of String just says that a string is a collection of characters whose size must be specified when a String variable is declared. Here’s what the declaration of String in the package Standard looks like:

    type String is array (Positive range <>) of Character;

Here the index subtype for String is defined to be a subtype of Positive. The symbol ‘<>’ is known as a box; it signifies that the exact range of values allowed is unspecified. This is referred to as an unconstrained array type; the actual range of values (the constraint) must be supplied whenever you declare a String variable so that the compiler knows how much memory to reserve to hold the string. One place where you are allowed to use an unconstrained type is as the type of a parameter in a procedure or function, which means that a procedure or function can be written to accept a string of any length as a parameter. This is acceptable because the compiler doesn’t have to allocate any memory in this case; the actual parameter will refer to a constrained string whose memory space has already been allocated. If an array type declaration specifies the size you lose this level of flexibility.

Arrays have a number of useful attributes which can be used to find out details about the index subtype:

    First    -- the lowest value of the index subtype
    Last     -- the highest value of the index subtype
    Range    -- the index subtype itself (First .. Last)
    Length   -- the number of elements in the array

These can be applied to the types themselves (if they’re constrained) or to individual array objects. The values of these attributes for the arrays Appt_List and Month_Length defined above would be as follows:

    Appt_List'First = 1             Month_Length'First = Jan
    Appt_List'Last = 100            Month_Length'Last = Dec
    Appt_List'Range = 1..100        Month_Length'Range = Jan..Dec
    Appt_List'Length = 100          Month_Length'Length = 12

The Range attribute can be used in situations where a range is expected, e.g. an array aggregate or a constraint in a declaration:

    Another_Name : String (Name'Range);
    And_Another  : String := (Name'Range => ' ');

We can declare Appt_Array as an unconstrained array in exactly the same way as String was declared:

    type Appt_Array is array (Positive range <>) of Appointment_Type;

To declare an Appt_Array, we have to provide the bounds of the range for the index subtype:

    Appt_List : Appt_Array (1..100);        -- as before

The range constraint ‘1..100’ is effectively used to fill in the box ‘<>’ which is used in the declaration of Appt_Array, so the index subtype for Appt_List is ‘Positive range 1..100’. Alternatively, an initial value in a declaration can be used to set the bounds of the index subtype:

    String_1 : String := "Hello world";     -- bounds are set by initial value

In this case, the bounds are set to be 1..11 because the initial value has these bounds. String literals and array aggregates whose bounds aren’t specified explicitly take their lower bounds from the lower bound of the index subtype; in the case of String the index is a subtype of Positive so the lower bound of a string literal is always Positive'First, i.e. 1. The initial value has to give the compiler enough information to figure out what the bounds are supposed to be, so an aggregate using others or the 'Range attribute won’t be acceptable.

The bounds of an array don’t have to be static; you’re allowed to calculate them at run time:

    Appts : Appt_Array (1..N);   -- N might be calculated when the program is run

A declare block can be useful if you want to calculate the array size at run time; the array size can be read in and the declare block can then use the value read in to create the array accordingly:

    Put ("How big do you want the array to be? ");
    Get (N);
    declare
        Appts : Appt_Array (1..N);
    begin
        ...
    end;

As you’ve seen with strings, you can supply the constraint as part of a subtype declaration and then use the subtype to declare variables:

    subtype Hundred_Appts is Appt_Array (1..100);
    Appt_List : Hundred_Appts;   -- same as: Appt_List : Appt_Array (1..100);

6.5 For loops revisited

A normal requirement with arrays is to process each element of the array in turn. This can be done using a for loop, as mentioned briefly in chapter 3:

    for P in Appt_List'First..Appt_List'Last loop
        Process ( Appt_List(P) );
    end loop;

Here P takes on successive values in the range Appt_List'First to Appt_List'Last each time around the loop. The first time around, P will be 1 (Appt_List'First), the second time it will be 2, and so on up to 100 (Appt_List'Last). The range specification in a for loop is just like the range specification for an array index subtype: it can be any discrete type or a range of values of a discrete type; as with an array index, a range like 1..100 which doesn’t mention a specific type will be assumed to be a range of type Integer.

There are a few significant points to note about for loops. The control variable P doesn’t have to be declared in advance; P is declared automatically just by its appearance as the control variable for the loop. In fact, if you do declare a variable called P you won’t be able to access it from inside the loop; using the name P inside the loop will refer to the control variable P rather than the variable called P declared outside the loop:

    with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
    procedure Test is
        P : Integer := 0;
    begin
        for P in 1..100 loop
            Put(P);        -- the control variable P
        end loop;
        Put(P);            -- the P declared at the beginning
    end Test;              -- (which is still 0)

Also, the control variable can’t be altered from inside the loop. This ensures that the number of times a for loop is executed is fixed in advance; you can still exit from the loop before it’s finished using an exit statement, but you can’t get stuck inside it forever. The control variable can’t be accessed outside the loop (it only exists while the loop is being executed); if you do use an exit statement and you need to find out what the value of the control variable was when you exited the loop, you’ll have to copy the control variable into an ordinary variable before executing the exit statement:

    with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
    procedure Test is
        I : Integer;
        A : array (1..100) of Integer;
    begin
        for P in 1..100 loop
            I := P;                        -- copy the value of P
            exit when A(P) = 0;            -- maybe exit the loop early
        end loop;               -- P no longer exists but I still does
        Put (I);                           -- the saved copy of P
    end Test;

The Range attribute provides a simple way of specifying the range of values in a for loop to process an array:

    for P in Appointment_List'Range loop
        Process ( Appointment_List(P) );
    end loop;

The range of values in a for loop, like the range of an array index subtype, can be any discrete subtype. Here are some examples:

    for P in 1..100 loop ...                            -- loop 100 times
    for P in -100..100 loop ...                         -- loop 201 times
    for P in Positive range 1..100 loop ...             -- loop 100 times
    for P in Day_Of_Week loop ...                       -- loop 7 times
    for P in Day_Of_Week range Monday..Friday loop ...  -- loop 5 times
    for P in 1..N loop ...                              -- loop N times

Note that the bounds of the range don’t have to be constants; N in the last example might be a variable. The value of N at the time the loop is started will determine how many times the loop will be executed. Note that changing the value of N inside the loop won’t change the number of times the loop is executed, which is determined by the value of N at the moment you enter the loop.

You can also go through a range of values in reverse order by specifying the word reverse before the index subtype:

    for P in reverse 1..100 loop ...

P will take on the values 100, 99, 98 and so on all the way down to 1. Note that the following won’t do what you might expect it to:

    for P in 100..1 loop ...

Since there is no range of values starting from 100 and ending at 1, the loop will not be executed (or rather, it will be executed zero times, which is the number of values over 100 which are less than 1). This is quite sensible; the following loop will be executed N times:

    for P in 1..N loop ...

If N is zero, the range of values will be 1..0, and so the loop will be executed zero times (i.e. it won’t be executed at all). If it didn’t behave this way and counted backwards from 1 to 0 you’d have to put in extra checks for the case where N is zero, which would make life much more complicated.


6.6 A simple sorting procedure

As a demonstration of using arrays, here’s a procedure to sort an array of integers into ascending order. It shows how to use array attributes, slices and both forward and reverse for loops. It uses a method known as shuffle sorting (which is not particularly efficient for large arrays, but it is only an example!). Shuffle sorting works by looking through the array for items which are in the wrong order (i.e. a smaller integer follows a larger one). When such an item is found, a copy of it is made and the array is then scanned backwards until the correct place for the item is found. The array elements from this point to the point where the value was originally are then moved along one place to leave a gap to slot the value into. Here’s the code for the procedure (which, since it uses a type called Array_Type, must be declared in the package or procedure where the declaration of Array_Type is given):

    type Array_Type is array (Positive range <>) of Integer;

    procedure Shuffle_Sort (X : in out Array_Type) is
        Position : Positive;
        Value    : Integer;
    begin
        for I in X'First+1 .. X'Last loop
            if X(I) < X(I-1) then
                -- Misplaced item found: copy it
                Value := X(I);
                -- Scan backwards until correct position found
                for J in reverse X'First .. I-1 loop
                    exit when X(J) < Value;
                    Position := J;
                end loop;
                -- Move intervening values along
                X(Position+1 .. I) := X(Position .. I-1);
                -- Put saved copy of item in correct position
                X(Position) := Value;
            end if;
        end loop;
    end Shuffle_Sort;

Notice how the procedure makes no assumptions about the values of the upper and lower bounds of the array; it uses the attributes First and Last to refer to them, which makes this procedure work with any array regardless of what its actual bounds are. You should always do this; never assume anything about the bounds of an array.

The innermost loop which searches backwards is an interesting one. It compares each item in turn with the saved item, starting with element I–1 and working back to the start of the array (X'First). The loop will always be executed at least once since I starts off as X'First+1; this means that I–1 cannot be less than X'First. Since we already know that element I–1 is greater than the saved item, the loop will always be executed in full at least once, so the value of Position will always be set. Position will end up holding the index of the last item which was greater than the saved item; if the loop terminates naturally rather than because of the exit statement, Position will be set to X'First and the saved item (which must be smaller than every other value before it if the exit statement was never triggered) will be slotted in at the very beginning of the array.


6.7 Multidimensional arrays

Ada allows arrays of any type of element. You can have arrays of integers or arrays of records; you can also have arrays of arrays. An array of strings is a good example of this:

    Strings : array (1..5) of String(1..5) :=
                ("SATOR", "AREPO", "TENET", "OPERA", "ROTAS");

The value of Strings(4) will be the single String "OPERA"; as usual, this can be subscripted to extract a single character from the string so that the letter P in "OPERA" could be referred to as Strings(4)(2).

An alternative to declaring arrays of arrays like this is to declare multidimensional arrays. A multidimensional array like the one above can be declared like this:

    Strings : array (1..5, 1..5) of Character :=
                       (('S', 'A', 'T', 'O', 'R'),
                        ('A', 'R', 'E', 'P', 'O'),
                        ('T', 'E', 'N', 'E', 'T'),
                        ('O', 'P', 'E', 'R', 'A'),
                        ('R', 'O', 'T', 'A', 'S'));

This declares Strings to be a 5×5 array of characters. The array aggregate is constructed as an array of array aggregates; hence the double parentheses. Individual elements are selected using a pair of subscripts, e.g. Strings(1,1) or Strings(1,5). The major difference between a two-dimensional array and an array of arrays is that you can only select individual elements of a two-dimensional array, but you can select an entire one-dimensional array from an array of arrays which you can then use in its entirety, or you can then slice it or subscript it like any other one-dimensional array.

You can have as many array dimensions as you like in a multidimensional array (subject to the overall limits on memory availability on your machine) and the index subtypes for each dimension can be different. For example, a chessboard consists of an 8×8 grid whose ranks (rows) are numbered 1 to 8 and whose files (columns) are lettered A to H, so that individual squares can be referred to as ‘e5’ or ‘g6’ or whatever. Here’s a declaration of a chessboard in Ada:

    type File is (A,B,C,D,E,F,G,H);
    type Rank is range 1..8;
    type Square is ... ;            -- some type declaration
    Board : array (File, Rank) of Square;

You can now refer to Board(E,5) or Board(G,6) and so on. If you wanted to, you could create a three-dimensional array to hold the positions after each of the first 40 moves like this:

    Game : array (File, Rank, 1..40) of Square;

You could then obtain the value of the e5 square on the 20th move by referring to Game(E,5,20).

When there is more than one dimension to the array, you have to specify which dimension you’re referring to when you use the attributes 'Range, 'First, 'Last and 'Length. You do this by appending the dimension number to the attribute name, e.g. Board'First(1) or Board'Last(2). Here are the values of the attributes of the array Board above:

    Board'Range(1)  = A..H     Board'Range(2)  = 1..8
    Board'First(1)  = A        Board'First(2)  = 1
    Board'Last(1)   = H        Board'Last(2)   = 8
    Board'Length(1) = 8        Board'Length(2) = 8

6.8 Discriminants

Sometimes you may want to use an array as a component of a record but you don’t necessarily want to tie yourself down to a specific array size. However, you can only use constrained arrays in a record type declaration so that the compiler knows how much space in memory to allocate when you declare a variable of that type. For example, consider a type to represent a variable-length string:

    type Varying_String is
        record
            Length : Natural;
            Value  : String (1..100);
        end record;

The idea here is that Value holds the string itself, which is restricted to a maximum of 100 characters, and that Length is used to record how much of the string is actually in use at any one time. The problem is if we want a different maximum length we have to redefine Varying_String. It would be much more convenient to be able to declare an ‘unconstrained’ record. The way to get around the problem is to use a discriminant in the record declaration. A discriminant must be a value of either a discrete type or an access type (which will be discussed in chapter 11):

    type Varying_String (Maximum : Positive) is
        record
            Length : Natural;
            Value  : String (1..Maximum);
        end record;

The discriminant effectively acts as a ‘parameter’ for the record. Varying_String is now an unconstrained type (like an unconstrained array, the compiler can’t work out how much space to reserve in memory for a Varying_String unless the value of Maximum is known), so when you declare a Varying_String you will have to specify a value for the discriminant Maximum:

    V1 : Varying_String (100);              -- a maximum of 100 characters
    V2 : Varying_String (Maximum => 50);    -- a maximum of 50 characters

You can also provide the discriminant in a subtype declaration:

    subtype Twenty is Varying_String (20);
    V3 : Twenty;                        -- a maximum of 20 characters

The discriminant can be accessed just like any other component of the record:

    V1.Length := V2.Maximum;            -- set V2's current length to 50

However, unlike other components, discriminants are constants; once they’ve been set in the declaration of the variable they can’t be changed:

    V2.Maximum := 100;                  -- ILLEGAL!

Aggregates used to initialise record types must provide values for all the components. Since the discriminant is a component, you have to provide a value for the discriminant in the aggregate:

    V4 : Varying_String := (Maximum=>20, Length=>5, Value=>"Hello ");

For convenience you can also provide a default value in the declaration of the original type:

    type Varying_String (Maximum : Positive := 80) is
        record
            Length : Natural;
            Value  : String (1..Maximum);
        end record;

    V1 : Varying_String;                -- default maximum of 80
    V2 : Varying_String (100);          -- explicit maximum of 100

You can also provide default values for other record components:

    type Varying_String (Maximum : Positive := 80) is
        record
            Length : Natural := 0;
            Value  : String (1..Maximum);
        end record;

Whenever a Varying_String is declared, the Value component will be 80 characters long by default and its Length component will be set to zero by default, so that newly created Varying_Strings will automatically be marked as containing zero characters but able to hold up to 80 characters. You can still set Length to a different value if you supply an initial value in the declaration:

    V1 : Varying_String;        -- Maximum = 80, Length = 0 by default
    V2 : Varying_String := (Maximum=>15, Length=>5, Value=>"Hello ");
            -- note that Value must be exactly Maximum characters long!

The defaults for record components don’t have to be constants, they can be any expression. Whenever a variable is declared the expression will be evaluated. You could for example include a timestamp in every record to keep track of the time it was created:

    type Time_Stamp is
        record
            Creation_Time : Ada.Calendar.Time := Ada.Calendar.Clock;
        end record;

In this case the function Clock from Ada.Calendar will be called when a Time_Stamp object is declared so that Creation_Time will be set by default to the time at which the object was created. Here’s another example: a record type to represent a bank account which is automatically given a unique account number when it is created:

    type Account_Number is range 1000_0000..9999_9999;
    type Money_Type is delta 0.01 range 0.00 .. 1_000_000_000.00;
    Last_Account : Account_Number := Account_Number_Type'First;

    function Next_Account_Number return Account_Number is
        New_Account : Account_Number := Last_Account;
    begin
        Last_Account := Last_Account + 1;
        return New_Account;
    end Next_Account_Number;

    type Bank_Account_Type is
        record
            Account : Account_Number := Next_Account_Number;
            Balance : Money_Type     := 0.00;
            -- and so on
        end record;

Now whenever an object of type Bank_Account_Type is created, the function Next_Account_Number will be called to initialise the Account component; this will use the value of Last_Account (so the first account created will be number 1000_0000) and it will also increment it (so the next account will be number 1000_0001, then 1000_0002, and so on). Note that Last_Account has to be declared outside Next_Account_Number so that it won’t lose its value between one call of Next_Account_Number and another.


6.9 Limited types

In the case of bank accounts, you may want to prevent one bank account object being assigned to another. If you don’t, assigning one account to another will result in two accounts with the same account number, and the original account number of the one being assigned to will have been lost. To prevent this, you can declare a record type to be limited:

    type Bank_Account_Type is
        limited record
            Account : Account_Number := Next_Account_Number;
            Balance : Money_Type     := 0.00;
            -- and so on
        end record;

Now you can declare variables of type Bank_Account_Type in the normal way:

    John, Fred : Bank_Account_Type;

The only operations available for ordinary record types are assignment (:=) and testing for equality or inequality (=, /=). Limited types don’t have these operations, so you can’t assign one bank account to another or test if they have the same value as each other. This also means that you can’t assign an initial value to a bank account in its declaration. You can, however, alter the individual components within the record; the prohibition on assignment and comparison only applies to the record as a whole. There is a way around this, but I’ll save it for later. Here are some examples involving the bank accounts John and Fred declared above:

    John         := Fred;            -- illegal!
    John.Balance := Fred.Balance;    -- legal (but is it sensible?)

Any array or record type involving limited components is automatically limited as well; you won’t be able to assign or compare a record containing a limited component since this involves assigning or comparing each individual component.


6.10 Using packages with data types

In most cases, the declaration of a record type will rely on a number of closely related type declarations used for the record components. These type declarations will all need to be kept together; if you use the record type you will usually need to use the component types as well. The simplest way to do this is to put the type declarations in a package. That way you can get access to the complete set of related type declarations by specifying the package name in a with clause. For example, here’s a package which contains the declarations of the types needed to represent dates:

    package JE.Dates is
        subtype Day_Type is Integer range 1..31;
        type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
        subtype Year_Type is Integer range 1901..2099;

        type Date_Type is
            record
                Day   : Day_Type;
                Month : Month_Type;
                Year  : Year_Type;
            end record;
    end JE.Dates;

Given this, any program which wants to use these declarations just has to specify JE.Dates in a with clause. Note that since the package does not contain any subprograms, there is no need for a package body since the specification does not include any incomplete declarations; in fact, you are forbidden to provide a package body unless the specification is incomplete in some way, e.g. if it declares any subprograms which then need to be defined in the package body.

If you want to define subprograms which have parameters of user-defined types, the type declarations must be available at the point where the subprogram is declared as well as at the point where it is called (so that the calling program can supply values of the correct type). The simplest way to do this is to put the type declarations and related subprograms together in a package so that the type declarations as well as the subprogram declarations are accessible to the calling program. Here is a modification of the date package from chapter 4 which uses Date_Type for the subprogram parameters instead of three individual integers, and which has Day_Of returning a result of the enumerated type Weekday_Type instead of an Integer:

    package JE.Dates is
        subtype Day_Type is Integer range 1..31;
        type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec);
        subtype Year_Type is Integer range 1901..2099;
        type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);

        type Date_Type is
            record
                Day   : Day_Type;
                Month : Month_Type;
                Year  : Year_Type;
            end record;
        function Day_Of (Date : Date_Type) return Weekday_Type;
        function Valid (Date : Date_Type) return Boolean;
    end JE.Dates;

The type declarations in the package specification are automatically visible in the package body so the package body will only need to contain the bodies of Day_Of and Valid. Any program which uses this package has access to the functions Day_Of and Valid as well as the type declarations it needs in order to use them.


Exercises

6.1 Write a program to count the number of occurrences of each letter of the alphabet typed as input at the keyboard. Using a subtype of Character as the index subtype of an array is a sensible way to do this.

6.2 Write a program which counts the number of occurrences of each word typed at the keyboard. Consider a word to be a sequence of up to 32 letters. You will need to use an array of records, where each record contains a word and the number of occurrences. Allow for a maximum of 100 different words, and ignore case differences. Functions to convert a character from upper case to lower case and to read the next word from the keyboard and return it as a string would be a good idea.

6.3 Produce a package defining data types to represent individual playing cards and also packs of cards (see exercise 5.4). Each card should be a record containing a suit and a value; the pack should contain an array of up to 52 cards (but try to avoid using the magic number 52 if you can!) together with the number of cards in the pack. Provide subprograms to initialise a pack (so that it contains a complete set of four suits of 13 cards each), to shuffle a pack (by interchanging each card with another randomly selected card from the same pack; see exercise 5.1), to deal a card (by removing the first card from the pack) and to replace a card (by adding it to the end of the pack). Write a simple test program to check that the package works correctly.

6.4 Write a program to encode a message using a simple substitution cipher, where each letter of the message is replaced by a different letter using a lookup table. The lookup table is generated using a keyword; the letters of the keyword are used for the first few positions in the table (ignoring any repeated letters) and the remaining letters of the alphabet are used to complete the table in alphabetical order. The output should be displayed in groups of five letters, all in upper case, with the last group padded out with Xs. For example, if the keyword is JABBERWOCKY, the lookup table will be as follows:
                A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
    encoded as: J A B E R W O C K Y D F G H I L M N P Q S T U V X Z

Note that B occurs twice in the keyword so the keyword actually appears as JABERWOCKY in the table, and the remaining letters of the alphabet (D, F, G, etc.) are then used to fill up the rest of the table. The message ‘Cabbages and kings’ would be encoded as BJAAJ ORPJH EDKHO PXXXX using this table.



Previous

Contents

Next

This file is part of Ada 95: The Craft of Object-Oriented Programming by John English.
Copyright © John English 2000. All rights reserved.
Permission is given to redistribute this work for non-profit educational use only, provided that all the constituent files are distributed without change.
$Revision: 1.2 $
$Date: 2001/11/17 12:00:00 $