Previous

Contents

Next

Chapter 5:
Defining new data types

So careful of the type she seems.
— Tennyson, In Memoriam


5.1 Standard data types
5.2 Integers
5.3 Subtypes
5.4 Derived types
5.5 Modular integers
5.6 Real types
5.7 Numeric literals
5.8 Constants
5.9 Enumerations
5.10 The type Boolean
5.11 The type Character
5.12 Renaming declarations
Exercises

5.1 Standard data types

As I said earlier, when you write a program you are constructing a model of some aspect of the real world, and the more closely that model reflects reality the better your program will be. Ada programs represent different types of real-world objects using different data types. As you’ve already seen, Ada takes quite a strict approach to data types, which allows the compiler to spot many errors that might otherwise be overlooked.

A data type specifies what values are possible for the corresponding real-world objects as well as specifying what operations can be performed on those objects. Ada provides a number of built-in data types to get you started, but it also recognises that it is impossible to provide data types to cater for every imaginable situation. The language provides you with the ability to define your own data types so that your program can model reality as accurately as possible. This also has the advantage that the more precise you are about the data in the real world that you’re modelling, the more the compiler can help you get your program right by checking it for errors.

The built-in data types are defined in a package called Standard which is always available automatically in every Ada program. Appendix B contains a listing of Standard. Unlike other packages, you do not have to use a with clause or a use clause to access the definitions provided in Standard. We have already met a few standard data types: the type Integer for dealing with whole numbers, the type String for dealing with sequences of characters, the type Character for dealing with individual characters, and the type Boolean for dealing with values which can be either True or False.

There are several other built-in types for dealing with numbers. Numbers in Ada are categorised as integers which are exact values with no fractional part (e.g. 123) or real numbers which have a fractional part (e.g. 1.23) but will not necessarily be represented with perfect accuracy; for example, the result of dividing 1.0 by 3.0 will be an infinitely long fraction 0.333333333333333... which can’t be represented exactly since we don’t have an infinite amount of memory. Also, since numbers are normally represented internally in binary, it will usually be impossible to represent 0.1 exactly, since this is a recurring fraction in binary.


5.2 Integers

The basic built-in integer type in Ada is called Integer. The exact range of numbers that type Integer can cope with is implementation defined; the only guarantee you have is that it will at least be able to hold values in the range ±32767. To allow you to find out the exact range of values available on a particular machine, all types in Ada have a set of attributes which can be used to discover various details about the type. Attributes are specified by putting an apostrophe (') and the attribute name after the type name. A complete list of attributes is given in Appendix C. In the case of integer types, the most important attributes are as follows:
    First           -- the first (lowest) value of the type
    Last            -- the last (highest) value of the type
    Image(X)        -- convert the integer X to a string
    Value(X)        -- convert the string X to an integer
    Min(X,Y)        -- the smaller of the integers X and Y
    Max(X,Y)        -- the larger of the integers X and Y
Thus Integer'Last will tell you what the largest value of type Integer is on your particular machine, and Integer'Image(X) will convert an Integer value X to a string. The following statement could be used to display the largest value of type Integer for a particular machine:
    Ada.Text_IO.Put ( Integer'Image(Integer'Last) );
Integer types come with a full set of arithmetic operations, some of which you’ve already seen:
    +    Addition           -    Subtraction
    *    Multiplication     /    Division
    rem  Remainder          mod  Modulus
    **   Exponentiation     abs  Absolute value
The + and – operators can be used as binary operators which produce a result computed from two operands (e.g. 5 + 7 or 5 – 7) or as unary operators which produce a result computed from a single operand (e.g. +5 or –5). The abs operator is also unary; it discards the sign of its operand leaving only the (positive) magnitude, so that abs 7 and abs –7 both give a result of 7.

As was briefly mentioned in chapter 4, operators are evaluated in order of precedence; multiplications are done before additions, and so on. The following table shows the precedence of all the operators in Ada (including some you haven’t been introduced to yet):

    Highest priority (evaluated first):  **    abs   not
                                         *     /     mod   rem
                                         +     -     (unary versions)
                                         +     -     &
                                         =     /=    <     >     <=    >=
    Lowest priority (evaluated last):    and   or    xor
Evaluation of a sequence of operators with the same precedence is done from left to right, so that 12/2*3 means (12/2)*3 = 6*3 = 18, rather than 12/(2*3) = 12/6 = 2. If you are in any doubt, use extra parentheses to make it absolutely clear what you intend.

The division operator produces an integer result when dividing integers, so that 7/5 would give a result of 1 rather than 1.4. The operators rem and mod allow you to find out the remainder resulting from a division, so that 7 rem 5 would give you a result of 2. The same result could be obtained by using mod; 7 mod 5 would also be 2. The difference between rem and mod is the way they deal with negative numbers: rem gives a negative result if the dividend (the left hand operand) is negative, whereas mod gives a negative result if the divisor (the right hand operand) is negative. Here are some examples which show the difference between them:

     7/5    =  1     7 rem 5     =  2     7 mod 5     =  2
    (-7)/5  = -1     (-7) rem 5  = -2     (-7) mod 5  =  3
     7/-5   = -1     7 rem -5    =  2     7 mod -5    = -3
    (-7)/-5 =  1     (-7) rem -5 = -2     (-7) mod -5 = -2
Note that –7 must be given in parentheses, as mod, rem and ‘/’ are evaluated before ‘–’. Without parentheses, ‘–7 rem 5’ would be interpreted as ‘–(7 rem 5)’.

With rem, the result is the conventional remainder, i.e. the difference between A and (A/B)*B. For example, 7/5 is 1. Multiply this by 5 and you get 5. The remainder is the difference between 7 and 5, i.e. 2. With mod, a multiple of B is added to the remainder if necessary so that the result is always between 0 and B, excluding B itself. So in the case of (–7)/5, the remainder of –2 produced by rem has to have 5 added to it so that the result of 3 is between 0 and 5.

The exponentiation operator raises a value to a given power, so that A ** B means A to the power B. For example, 4 ** 3 is 43 (4 cubed), i.e. 64. The value of the right hand operand of ** cannot be negative since this will not give an integer result.

The standard type Integer may or may not provide a large enough range of values for what you need. For example, you might want to represent a time of day as a number of seconds since midnight, which requires a range of values between 0 and 86399. On some machines the type Integer might be able to represent values as high as 86399 but there’s no guarantee that it will. However, it is easy enough to define your own integer types; all you have to do is to write a type declaration like this:

    type Time_Of_Day is range 0..86399;
You now have a new data type called Time_Of_Day which is an integer type and which will have the same attributes and operators as the built-in type Integer. However, as far as Ada is concerned, each type declaration creates a brand new type which is unrelated to all the other types (even if the range of values is identical), so you can’t mix Integer values with Time_Of_Day values. In other words, if I is an Integer and T is a Time_Of_Day, the following statements are illegal:
    T := I;      -- can't assign Integer to Time_of_Day
    I := T;      -- can't assign Time_Of_Day to Integer
    T := T + I;  -- can't add Integer to Time_Of_Day
    T := I + I;  -- can't assign Integer to Time_of_Day
Operations such as + in the last statement above return a value of the same type as their operands (well, the base type of their operands actually, as described later; this is basically the same thing except that the range of values might be less restricted), so the addition I + I is legal and produces an Integer as its result. However, this can’t be assigned to a Time_Of_Day variable because the types don’t match. The problem doesn’t arise with integer literals such as 1 or 99; these are universal integers which can be used with any integer type, so the following are all legal:
    T := 99;
    I := 99;
    T := T + 99;
    T := 99 + 99;
In the last example, adding two universal integers together gives a universal integer result.

Any numeric type can be converted to any other numeric type using a type conversion (which will involve rounding a real value to the nearest integer if you are converting a real type to an integer type). A type conversion consists of the name of the type you want to convert to, followed by the value to be converted enclosed in parentheses. The errors shown above can be avoided by using type conversions where necessary:

    T := Time_Of_Day (I);
    I := Integer (T);
    T := T + Time_Of_Day (I);
    T := Time_Of_Day (I + I);
Of course, the value might be out of the legal range of the target type; for example, I might be negative, in which case there is no corresponding Time_Of_Day value that it can be converted to. If this happens, a Constraint_Error exception will be raised.

If you want to display Time_Of_Day values on the screen or read them from the keyboard, you can’t use Ada.Integer_Text_IO since this is for use with type Integer and type Time_Of_Day is quite distinct from this. You can always use Time_Of_Day'Image to convert a Time_Of_Day value to a String that you can display with Ada.Text_IO.Put:

    Ada.Text_IO.Put( Time_Of_Day'Image(T) );
but a better way is to create your own input/output package for Time_Of_Day that provides the same facilities as Ada.Integer_Text_IO. This is easy to do; here’s the declaration of a package called Time_Of_Day_IO which will give you the same facilities for Time_Of_Day values that Ada.Integer_Text_IO does for Integers:
    package Time_of_Day_IO is new Ada.Text_IO.Integer_IO (Time_Of_Day);
These lines can be put in the declaration section of a procedure after the declaration of Time_Of_Day itself. Ada.Text_IO must have been named in a with clause for this to work. What it does is to create a new package which is a copy of the generic package Ada.Text_IO.Integer_IO. This provides input/output facilities for integer types in general; all you have to do is to say which specific integer type you want to use it for. In the official terminology, we have instantiated (i.e. created a new instance of) the package Ada.Text_IO.Integer_IO for use with Time_Of_Day values. The standard package Ada.Integer_Text_IO is effectively predefined for use with Integers by the same process. The listing of Ada.Text_IO in Appendix B contains the definition of Integer_IO, so you can refer to that to find out what Ada.Integer_Text_IO provides.

5.3 Subtypes

The Ada notion of types is central to the issue of developing reliable software. If two integers represent quantities of chalk and cheese, we don’t really want to be able to mix them indiscriminately. We want the compiler to be able to say ‘sorry, you seem to be mixing chalk and cheese; what do you really mean?’. And of course if that’s what you really want to do you can, but you have to tell the compiler that you’re doing it deliberately rather than accidentally with an explicit type conversion. This seems to be an unnecessary complication to people who are used to other, less strict, programming languages, and you can usually spot code written by programmers who are new to Ada because they use Integer for absolutely everything. It takes time to get used to the idea of analysing what you’re representing and going to the extra bother of writing a type declaration and doing type conversions where necessary. Once you do, the compiler can help you to spot all sorts of silly mistakes arising from muddled thinking. The extra effort is beneficial because it gives the compiler the means to check your programs for errors in a much more thorough way than it would otherwise be able to do; but since it’s an extra effort it requires self-discipline on your part. However, self-discipline is essential if you’re going to be a good programmer!

Sometimes, though, we may want to specify that a particular type of variable should hold a restricted range of the values covered by some other type without creating a brand new type which would need type conversion to be used in conjunction with the original type. We want to be able to mix different types freely when they’re just different aspects of the same thing, but we still want the benefits which arise from telling the compiler what we’re trying to do so that it can check if we’re doing it right. For example, we want to specify that the right hand operand in an integer exponentiation is non-negative so that we don’t end up with a real result. Ada allows us to define subtypes of existing types which behave just like the original type except that they have a restricted range of values:

    subtype Natural  is Integer range 0..Integer'Last;
    subtype Positive is Integer range 1..Integer'Last;
These declarations define two subtypes of type Integer: Natural is an Integer which cannot be less than zero, and Positive is an Integer which cannot be less than 1. In fact, both these subtypes are useful enough that they are already provided as built-in types declared in the package Standard, and the exponentiation operator is defined so that it requires a Natural value on its right hand side. Subtypes of a type can be used anywhere that the type itself or any of its subtypes can be used, so you can use a Natural variable anywhere an Integer is required and vice versa; however, if you use an Integer where a Natural is required the compiler will automatically insert checks into your program to ensure that the Integer isn’t negative. If it is negative, a Constraint_Error exception will be raised. This lets you separate out the error handling into the exception handler section, which is more readable than having the error checking code jumbled together with the code for normal processing.

You don’t have to restrict the range of values in a subtype declaration, as was done in the declarations of Natural and Positive:

    subtype Whole_Number is Integer;
This means that Whole_Number has the same range of values as Integer, so it is effectively just another name for the same type.

To avoid raising exceptions you can test if a value is within the range of a particular subtype using the in and not in operators. For example, if I is an Integer variable and N is a Natural variable, you can test if I can be assigned to N like this:

    if I in Natural then
        N := I;            -- OK, I is in Natural's range
    else
        Put_Line ("I can't be assigned to N!");
    end if;
All types in Ada are actually subtypes of anonymous types known as their base types, which I alluded to briefly above. Since the base types are anonymous you can’t refer to them directly by name, but you can use the 'Base attribute to get at them; for example, Integer'Base is the base type of Integer. Base types may or may not have a wider range of values than their subtypes; the only significance of this is that intermediate values in expressions like A*B/C use the base type so that A*B might be able to exceed the limits of the type without raising an exception as long as the final result is within the required limits.

5.4 Derived types

Sometimes it’s useful to define a new type in terms of an existing type, like this:
    type Whole_Number is new Integer;
This defines a new type called Whole_Number which has exactly the same properties as Integer. Whole_Number is said to be derived from Integer; Integer is referred to as Whole_Number’s parent type. The range of values and the operations available will be the same for Whole_Number as for Integer (Whole_Number is said to inherit all the operations of its parent type), but unlike the subtype declaration for Whole_Number shown earlier, Whole_Number will be a completely different type to Integer. However, it is always possible to use a type conversion to convert from a derived type to its parent type and vice versa. This means that if you want to mix Whole_Numbers and Integers in an expression, you will have to use a type conversion to convert one type to the other:
    I : Integer;
    W : Whole_Number := Whole_Number(I);  -- convert Integer to Whole_Number
The derived type declaration can also include a range constraint:
    type Age is new Natural range 0..150;
Now Age is the name of a new integer type derived from Natural but restricted to values between 0 and 150.

Derivation creates a family of related types usually referred to as a class; for example, all integer types belong to the class of integer types. Also, you’ll see later that the class of integer types is part of a larger class, the class of discrete types. The main reason for creating derived types is in situations where extra ‘primitive operations’ have been defined for a particular type. You could create a new type and then define an identical set of extra operations, but by using derivation you automatically inherit versions of all the primitive operations of the parent type so no rewriting is needed. Thus the class of discrete types provides the attribute 'First which all discrete types will inherit; the integer class adds arithmetic operations like ‘+’ which integer types then inherit in addition to the properties they inherit by being discrete types. This is a subject that will be explored more fully in later chapters in connection with tagged types.


5.5 Modular integers

The integer types we have seen so far are properly known as signed integer types. These are defined by specifying the range of values allowed for that type. In the case of Time_Of_Day, this doesn’t do quite what we want. Imagine that the time is one second before midnight, which would be stored in a Time_Of_Day variable as the value 86399. What happens if we try to add 1 to it? We will get the value 86400 which is outside the range of values allowed for a Time_Of_Day variable and a Constraint_Error will be reported as described above. What we really want here is for the value of the variable to wrap around from 86399 (one second to midnight) back to 0 (midnight).

One way to do this would be to do all arithmetic modulo 86400 using the mod operator described earlier:

    T := (T + 1) mod 86400;
This is a bit risky, since evaluating T + 1 might give rise to a constraint error (although it’s unlikely in this particular case; 86400 will almost certainly be within the range of the base type). A better way to do this would be to define Time_Of_Day as being a modular integer type:
    type Time_of_Day is mod 86400;
Now all arithmetic on Time_Of_Day values is performed ‘mod 86400’ so that adding 1 to 86399 will wrap around back to 0, and subtracting 1 from 0 will wrap around to 86399. As a result arithmetic on modular integers will never raise a constraint error. However, the same is not true for type conversions. Attempting to convert a value outside the range 0 to 86399 to a Time_Of_Day value will still raise a Constraint_Error. Modular types provide an attribute called Modulus which gives the modulus of the type, so that Time_Of_Day'Modulus would give 86400. You can use this with the mod operator to ensure that values are in the correct range before assigning them to Time_Of_Day variables.

Text_IO provides a generic package for input and output of modular integers called Ada.Text_IO.Modular_IO. The following line can be used to instantiate Modular_IO for use with Time_Of_Day values, after which you’ll have Get and Put procedures for Time_Of_Day values just like the ones for Integer values in Integer_Text_IO:

    package Time_Of_Day_IO is new Ada.Text_IO.Modular_IO (Time_Of_Day);

5.6 Real types

Ada provides two ways of defining real types. Floating point types have a more or less unlimited range of values but are only accurate to a specified number of decimal places; fixed point types have a more limited range but are accurate to within a specified amount (the delta of the type). You can also have decimal types, but Ada compilers are not required to implement them so I’m not going to bother discussing them in detail. There is one standard floating point type called Float which is accurate to at least six significant figures and there is one standard fixed point type called Duration (used for representing times) which is accurate to at least the nearest 50 microseconds (a delta of 0.00005 seconds) up to a maximum of at least 86400.0 seconds (24 hours). There are no standard decimal types. You can define your own real types like this:
    type My_Float is digits 10;                     -- a floating point type
    type My_Fixed is delta 0.01 range 0.0 .. 10.0;  -- a fixed point type
    type Decimal  is delta 0.01 digits 12;          -- a decimal type (delta
                                                    -- must be a power of 10)
Here My_Float is a floating point type which is accurate to at least ten significant figures, and My_Fixed is a fixed point type which is accurate to within 0.01 (i.e. to at least two decimal places) across the specified range. Decimal is a decimal type with 12 digits which is accurate to two decimal places (i.e. capable of representing decimal values up to 9999999999.99). You can find out the digits value of a floating point type by using the Digits attribute (e.g. Float'Digits) and the delta value of a fixed point type by using the Delta attribute (e.g. Duration'Delta). Many of the attributes already described for integers (First, Last, Image, Value and so on) also apply to real types; for a complete list of attributes which apply to real types, refer to Appendix C.

You can also have subtypes of real types; for example, there is a standard package called Ada.Calendar which defines a subtype of Duration called Day_Duration like this:

    subtype Day_Duration is Duration range 0.0 .. 86400.0;
The same arithmetic operators are available for real numbers as for integers, except that dividing two real numbers gives a real result and so the mod and rem operators are not defined for real numbers. Also the exponentiation operator can be used to raise a real number to any integer power; raising a real number to a negative power will produce a real result, so the right hand operand is no longer restricted to belonging to the subtype Natural as it is for integer types.

There is a standard package called Ada.Float_Text_IO which provides Get and Put procedures for the standard type Float. You can also create your own for use with other real types; Ada.Text_IO provides two generic packages for input/output of floating point and fixed point values called Float_IO and Fixed_IO respectively. Ada.Float_Text_IO is effectively just an instantiation of Ada.Text_IO.Float_IO for type Float, so you can look at the listing of Ada.Text_IO in Appendix B to find out the details of Ada.Float_Text_IO. The specification of Put in this package is somewhat different to Put for integer types; in Ada.Float_Text_IO it looks like this:

    procedure Put (Item : in Float;
                   Fore : in Field := Default_Fore;
                   Aft  : in Field := Default_Aft;
                   Exp  : in Field := Default_Exp);
The optional parameters Fore, Aft and Exp can be used to control the layout of the values displayed on the screen. The default for floating point types is to display the number with a three-digit exponent, so that 1234.5678 would be displayed as 1.2345678E+003 (meaning 1.2345678 × 103); with fixed point values it would be displayed as 1234.5678, possibly with some extra spaces before and zeros afterwards. Fore specifies how many characters to display before the decimal point, Aft specifies how many digits to display after the decimal point (which will cause the value to be rounded to that many places if necessary) and Exp specifies how many digits there are in the exponent; a value of zero means that no exponent will be displayed. Here are some examples of how the version of Put for floating point values works:
    Put (1234.5678);                            -- displays " 1.2345678E+003"
    Put (1234.5678, Exp=>0);                    -- displays "1234.5678"
    Put (1234.5678, Fore=>5);                   -- displays "    1.2345678E+003"
    Put (1234.5678, Fore=>5, Aft=>2, Exp=>0);   -- displays " 1234.57"
    Put (1234.5678, Fore=>5, Aft=>2);           -- displays "     1.23E+003"
Note that displaying an exponent means that the value is ‘normalised’ so that there is only one digit before the decimal point. Also, unlike the version of Put for integer types, real numbers can only be displayed in decimal; there is no equivalent of the Base parameter.

5.7 Numeric literals

To wind up the discussion of numeric types, let’s look at how numbers are written in Ada. We’ve already seen integer values such as 86400 and real values such as 3.14159265; real values have a decimal point whereas integers don’t. You can also use underline characters within numbers to improve readability (e.g. 86_400 or 3.14159_26536). What’s more, you can use exponential notation: 86400 can be written as 864e2, meaning 864 × 102, and 3.14159_26536 can be written as 31415.926536e–4, meaning 31415.926536 × 10–4. As usual, it doesn’t matter whether you use upper or lower case; 864e2 and 864E2 mean exactly the same thing.

It’s also possible to write numbers in binary or hexadecimal or any other base between 2 and 16. Here are three different ways of writing the decimal value 31:

    2#11111#  -- the binary (base 2) value 11111
    16#1F#    -- the hexadecimal (base 16) value 1F
    6#51#     -- the base 6 value 51
The letters A to F (or a to f) are used for the digits 10 to 15 when using bases above 10. If you mix an exponent (e) part with a based number, the exponent is raised to the power of the base; thus 16#1F#e1 means hexadecimal 1F (= 31) × 161, or 496 in decimal.


5.8 Constants

We can use the package Ada.Calendar mentioned earlier to improve on our good morning/good afternoon program. Instead of asking the user to tell us whether it’s morning or afternoon, why not ask the computer? Ada.Calendar contains (among other things) a function called Clock whose result is the current date and time. This result is of type Time which is defined in Ada.Calendar. There is also a function called Seconds which takes a Time as its parameter and extracts the time of day, returning the number of seconds since midnight as a value of type Day_Duration which was mentioned earlier. Here are the specifications for Clock and Seconds:
    function Clock return Time;
    function Seconds (Date : Time) return Day_Duration;
This tells us that Clock is a function with no parameters which returns a result of type Time, and Seconds is a function with a parameter called Date of type Time which returns a Day_Duration result. All we have to do is to use Seconds to extract the time of day from the value produced by Clock and check if it is after noon (43200.0 seconds since midnight). Here is the new version of the program:
    with Ada.Text_IO, Ada.Calendar;
    use Ada.Text_IO;
    procedure Greetings is
    begin
        if Ada.Calendar.Seconds (Ada.Calendar.Clock) <= 43200.0 then
            Put_Line ("Good morning!");
        else
            Put_Line ("Good afternoon!");
        end if;
    end Greetings;
The value 43200.0 in the new version of the program is remarkably uninformative. It would make the program much more readable if we used a name like Noon instead of this ‘magic number’. This is generally true for practically all numbers except 0 and 1. Numbers almost always represent a quantity of something, and should therefore be given names which indicate what that something is. In many cases they are also subject to change as part of the maintenance process and should therefore be defined at a single place in the program so that any necessary change can be accomplished by altering just one line in the program. This can be done by defining named numbers like this:
    Minute : constant := 60;
    Hour   : constant := 60 * Minute;  -- i.e. 3600
These names can be used in exactly the same way as the numbers they stand for. They are universal integers just like the numbers 60 and 3600 so that they can be used whenever an integer of any type is needed. Universal real numbers can be used in exactly the same way. Although a named number declaration looks just like a variable declaration, the reserved word constant indicates that these values cannot be altered:
    Hour := Hour + 1;    -- illegal!
Named numbers can be used anywhere that the corresponding numeric literal could be used, e.g. in a type declaration:
    type Time_Of_Day is mod 24 * Hour;   -- same as "mod 86400"
One thing to remember is that in order for a named number to be usable anywhere that the corresponding ‘magic number’ could be used, the compiler must be able to work out its value at compile time (i.e. when the program is being compiled). This doesn’t rule out using arithmetic expressions; for example, the declaration of Hour uses the expression 60*Minute as its value and Time_Of_Day uses 24*Hour as the modulus of the type. This is perfectly all right provided that the compiler can work out the value of the expression; in particular, it needs to know how much memory a Time_Of_Day object will require. An expression like this that can be evaluated at compile time is known as a static expression, meaning that its value is not dependent on extraneous factors such as input from the user or the time of day at which the program is being run. In this case the expression 24*Hour depends on knowing what Hour is at compile time, which in turn depends on knowing what Minute is. Since the compiler knows that Minute means 60, it can work out that Hour is 3600 and thus 24*Hour is 86400.

It is also possible to define constant values of a particular type by specifying the type name as part of the declaration:

    Noon  : constant Day_Duration := Day_Duration (12 * Hour);
    Start : constant Day_Duration := Seconds (Clock);
Named numbers must be static, but constants of a specific type like Start can have values which aren’t known until run time (i.e. when the program is run); in the case of Start, its value will be the time at which it is declared. Variables and constants are collectively referred to as objects in Ada. Here is another version of the program, modified to show the use of some constants:
    with Ada.Text_IO, Ada.Calendar;
    use  Ada.Text_IO, Ada.Calendar;
    procedure Greetings is
        Minute : constant              := 60;
        Hour   : constant              := 60 * Minute;
        Noon   : constant Day_Duration := Day_Duration (12 * Hour);
        Start  : constant Day_Duration := Seconds (Clock);
    begin
        if Start <= Noon then
            Put_Line ("Good morning!");
        else
            Put_Line ("Good afternoon!");
        end if;
    end Greetings;
Although this may seem quite long-winded, that’s only because it’s such a short example. If you compare the if statement above to the one in the previous example, I’m sure you’ll agree that it’s much easier to see exactly what this version is trying to achieve.

In many cases you can use attributes instead of constants to avoid using magic numbers. For example, you could use a constant to define type Time_Of_Day like this:

    Maximum : constant := 86399;
    type Time_Of_Day is range 0..Maximum;
and then you could use Maximum wherever you needed to refer to the largest possible Time_Of_Day value. But since Time_Of_Day'Last will give the same value as Maximum, why not just define Time_Of_Day like this:
    type Time_Of_Day is range 0..86399;
and then use Time_Of_Day'Last wherever you would use Maximum or (perish the thought) 86399?


5.9 Enumerations

In many cases numbers are unsuitable for representing the types of data required by a program. Consider the days of the week as an example. We could use the numbers 0 to 6 or 1 to 7 to represent the days of the week, but they don’t lend themselves to this naturally. The natural way to represent days of the week is by using their names (Monday, Tuesday, Wednesday and so on). We could of course define constants with the appropriate values, like this:
    type Day_Of_Week is range 0..6;    -- or "mod 7" perhaps

    Sun : constant Day_Of_Week := 0;
    Mon : constant Day_Of_Week := 1;
    Tue : constant Day_Of_Week := 2;
    Wed : constant Day_Of_Week := 3;
    Thu : constant Day_Of_Week := 4;
    Fri : constant Day_Of_Week := 5;
    Sat : constant Day_Of_Week := 6;
The disadvantage with this is that it would allow us to perform arithmetic on days of the week; for example, what do the expressions Wednesday + Tuesday or Monday * 2 mean?

Enumeration types allow us to define types as a list which enumerates the possible values of the type. We could define the type Day_Of_Week as an enumeration type like this:

    type Day_Of_Week is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
This says that a Day_Of_Week object has seven possible values whose names are Sun, Mon, Tue and so on. You can compare values of an enumeration type (using =, /=, <, <=, > and >=) and assign them, but operations like addition and subtraction are not provided. For example, assuming D is a Day_of_Week variable, you can do the following:
    D := Mon;
    if D = Mon then
        Put_Line ("Oh no, it's Monday again...");
    end if;
The ordering of the values is that defined by the list of values you provide, so that Sun is less than Mon and so on. There are a number of useful functions provided as attributes for enumeration types in addition to the ones mentioned earlier for integer types (First, Last, Image and Value):
    Pos(X)    -- an integer representing the position of X in the
                 list of possible values starting at 0
    Val(X)    -- the X'th value in the list of possible values
    Succ(X)   -- the next (successor) value after X
    Pred(X)   -- the previous (predecessor) value to X
These can actually be used with integer types as well, but they aren’t a lot of use since Pos and Val will convert an integer to itself and Succ and Pred can be replaced by addition and subtraction. They are only really useful for enumeration types. Pos and Val allow you to convert enumerations to integers and vice versa, while Succ and Pred effectively allow you to add or subtract 1 from an enumeration value. Here are a few examples:
    Day_Of_Week'Pos(Sun)  = 0       Day_Of_Week'Pos(Wed)  = 3
    Day_Of_Week'Val(0)    = Sun     Day_Of_Week'Val(3)    = Wed
    Day_Of_Week'Succ(Mon) = Tue     Day_Of_Week'Pred(Fri) = Thu
Note that you will get a constraint error if you try to evaluate anything like Day_Of_Week'Val (7), Day_Of_Week'Succ (Sat) or Day_Of_Week'Pred (Sun) since in all these cases you are going outside the limits of the range of values available.

It is sometimes useful to use the same name for an enumeration value for two unrelated types. Here is a slightly artificial example:

    type Weekday  is (Sun, Mon, Tue, Wed, Thu, Fri, Sat);
    type Computer is (IBM, Apple, Sun, Cray);
Note that the name Sun is used as a value for Weekday as well as Computer. This shows that enumeration literals, like subprogram names, can be overloaded. The compiler will normally be able to distinguish between them from the type of value it expects to see at a particular point in the program. In the rare cases when it can’t it will report an error, and you will then have to specify explicitly whether you mean Sun of type Weekday or of type Computer like this:
    Weekday'(Sun)   -- the value Sun of type Weekday
    Computer'(Sun)  -- the value Sun of type Computer
This looks similar to a type conversion but it isn’t; the apostrophe between the type name and the parenthetical expression shows that this is a qualified expression which just tells the compiler what type you expect the parenthetical expression to have. No conversion is performed:
    F1 : Float := Float(123);   -- type conversion from Integer to Float
    F2 : Float := Float'(123);  -- qualified expression will be reported as an
                                -- error since 123 isn't a valid Float value
The integer and enumeration types are collectively known as discrete types since they define a set of discrete values which can be listed in order. Real numbers can’t be listed in this way since there are (in theory at least) an infinite number of them between any two real values you care to choose. Discrete types play a special role in various circumstances where discreteness is a useful property; I’ll return to this point later. (A table showing the hierarchy of the types available in Ada and the relationships between them is given at the end of Appendix A.) Like the packages for input and output of the numeric types, there is a generic package called Ada.Text_IO.Enumeration_IO that you can instantiate for input/output of enumeration types:
    package Day_Of_Week_IO is
                 new Ada.Text_IO.Enumeration_IO (Day_Of_Week);
This will provide Get and Put procedures for Day_Of_Week values. Put will by default display the enumeration value in upper case in the minimum possible width, but there are optional parameters Width and Set you can use to alter this. Here are some examples:
    Put (Sun);                    -- displays "SUN"
    Put (Sun, Width=>6);          -- displays "SUN   "
    Put (Sun, Set=>Lower_Case);   -- displays "sun"
Unfortunately there is no ‘Capitalised’ value for the Set parameter which would display it as ‘Sun’; the only possibilities are Upper_Case (giving ‘SUN’) and Lower_Case (giving ‘sun’).

We could use an enumerated type in a variant of the Greetings program from the previous chapter. Instead of asking the user to type M or A, we could define an enumeration type like this:

    type Time_Of_Day is (AM, PM);
If we instantiate Enumeration_IO for use with type Time_Of_Day the user could then type in either AM or PM in either upper or lower case (or any mixture of the two) with or without leading spaces (which previous versions of the program didn’t allow). Here’s the reworked program:
    with Ada.Text_IO;
    use  Ada.Text_IO;
    procedure Greetings is
        type Time_Of_Day is (AM, PM);
        package Time_IO is new Enumeration_IO (Time_Of_Day);
        use Time_IO;
        Answer : Time_Of_Day;
    begin
        Put ("Is it morning (AM) or afternoon (PM)? ");
        Get (Answer);
        if Answer = AM then
            Put_Line ("Good morning!");
        else
            Put_Line ("Good afternoon!");
        end if;
    end Greetings;
As with integers you can define subtypes of enumerated types:
    subtype Working_Day is Day_Of_Week range Mon .. Fri;
Now you can use Working_Day wherever Day_Of_Week can be used but the values allowed are limited to those between Mon and Fri inclusive.


5.10 The type Boolean

The type Boolean is yet another one of Ada’s standard data types, and is an enumeration type declared in the package Standard as follows:
    type Boolean is (False, True);
Boolean plays a special role in Ada; it’s used in the conditions of if and exit when statements as well as a few other places. Note that if you try putting a declaration for Boolean (or any other standard type) in your program you will be creating a brand new type; types in Ada which have different names are different even if their declarations are identical, and the full name for the standard Boolean type is Standard.Boolean. You will end up with two completely separate types called Boolean and Standard.Boolean, and since if statements and the like require conditions of type Standard.Boolean you won’t be able to use your own Boolean type in this sort of context.

Comparison operators like ‘=’ produce a Boolean result. The comparison operators available are as follows:

    A = B        -- True if A is equal to B
    A /= B       -- True if A is not equal to B
    A < B        -- True if A is less than B
    A <= B       -- True if A is less than or equal to B
    A > B        -- True if A is greater than B
    A >= B       -- True if A is greater than or equal to B
These can be used to compare values of any of the types described in this chapter. There are some other operators which combine Boolean values to produce Boolean results. We’ve already seen how or can be used; here is the full list:
    A or B     -- True if either or both of A and B are True
    A and B    -- True if both A and B are True
    A xor B    -- True if either A or B is True (but not both)
    not A      -- True if A is False
And, or and xor have the same precedence; if you want to mix them (e.g. using and and or together) in the same expression you must use parentheses to make the meaning unambiguous:
    A and B or C      -- illegal due to ambiguity
    (A and B) or C    -- one possible legal interpretation
    A and (B or C)    -- the other possible legal interpretation
There are also variants of and and or to cater for a few tricky situations. Consider this situation as an example:
    if B /= 0 and A/B > 0 then ...
The problem with this is that the expression on the right of the and operator is always evaluated, so that when B is zero the expression A/B will still be evaluated with the result that the division by zero will cause a constraint error to be raised. However, this is presumably what the check on B’s value is supposed to avoid! The solution is to use the operator and then instead of and:
    if B /= 0 and then A/B > 0 then
    ...
And then only evaluates its right hand side if it needs to; if B is zero, the overall result of the Boolean expression must be false so the right hand side won’t be evaluated. This means that the division by zero won’t happen, so a constraint error won’t occur. The right hand side will only be evaluated if B is non-zero, in which case it’s safe to divide A by B. The equivalent for or is or else, which only evaluates its right hand side if the expression on the left hand side is false:
    if B = 0 or else A/B <= 0 then
    ...
You can of course define variables and constants of type Boolean:
    Morning : constant Boolean :=
    Start < Noon;
This refers to the constants Start and Noon defined earlier. Morning will be True if the program is run before noon and False otherwise. Boolean variables or constants can be used directly in if statements, while loops and any other context that expects a Boolean value:
    if Morning then ...        -- same as
    "if Start < Noon then ..."
One common beginner’s mistake is to write if statements involving Boolean values like this:
    if Morning = True then ...
This is of course redundant; if you do this you are asking if True is equal to True, and if it is the result is True! Likewise, these are two different ways of saying the same thing:
    if Morning = False then ...
    if not Morning then ...
The second version is considered better style; it is certainly more easily understood than the first. A similar situation arises when assigning values to Boolean variables. Beginners sometimes write things like this:
    if Start < Noon then
        Morning := True;
    else
        Morning := False;
    end if;
but you can achieve the same effect in a much less long-winded way, like this:
    Morning := Start < Noon;
If Start is less than Noon, the expression ‘Start < Noon’ will evaluate to True, so Morning will be assigned the value True; if not, it will be assigned the value False.


5.11 The type Character

The other standard enumeration type is Character. This is an enumeration type whose values are the list of characters defined by the International Standardization Organization (ISO) in the standard ISO-8859. There are 256 possible characters in this character set which include letters, digits, punctuation marks, mathematical symbols and characters like å, ß and ç which are required in some European languages. The definition of type Character is given in the package Standard in Appendix B; this shows the characters it provides.

Some of the characters have no printable value; they are used as control characters. Examples include the ‘carriage return’ character which moves the cursor to the left of your screen when you display it and the ‘form feed’ character which is used to start a new page on a printer. To allow you to refer to them there is a package called ASCII (for American Standard Code for Information Interchange, the predecessor to ISO-8859) defined as part of the package Standard which provides names for these. Since ASCII is defined inside Standard you don’t need to specify it in a with clause before you can use it. As a result, you can always refer to the carriage return character as ASCII.CR and the form feed character as ASCII.FF. However, this is a historical remnant from Ada 83; it only provides names for the first 128 characters of ISO-8859 and it might not be provided at all in future versions of the language. For these reasons it is better (if slightly less convenient) to use the package Ada.Characters.Latin_1 instead. This gives names for all the 256 available characters but it must be included using a with clause. Appendix B contains a listing of Ada.Characters.Latin_1.

The 256 characters are sufficient for European languages but doesn’t cater for languages like Japanese or Russian. Ada provides another type Wide_Character which is similar to Character except that it provides 65536 different characters. There is also a type Wide_String corresponding to String; type String is a sequence of Characters, and Wide_String is a sequence of Wide_Characters.

As well as defining enumeration types using names like Sunday or Monday for the values you are allowed to use character literals like '+' or '*'. The declaration of Character in the package Standard makes use of this to define the set of printable characters. Here’s the declaration of a data type which represents the operators used in the previous chapter’s calculator program:

    type Operator is ('+', '-', '*', '/');
Note that this is a completely different data type to Character, and there is no way to do a straight type conversion from Character to Operator or vice versa. Also, if you instantiate Enumeration_IO for Operator you might be in for a nasty shock; the values will be displayed complete with the enclosing quotes, and you must also type the quotes on input. This makes this facility somewhat less useful than it might otherwise be.


5.12 Renaming declarations

One of the things that contribute to maintainability is readability, and one of the things that contribute to readability is the use of meaningful names. However, most programmers find it a bit of a chore to write meaningful names, so that variables with short, almost meaningless names like I and J are quite common. You may have noticed that I do it myself from time to time; that’s human nature. However, I tend to do it only where the name is going to be used in a small section of program where I’m not going to have to refer back across hundreds of lines to find out whether I stands for Interest_Rate or Idiocy_Ratio. Fully qualified names defined in external packages can be especially inconvenient to use; writing a name like Ada.Characters.Latin_1.ESC or even Ada.Characters.Latin_1.Registered_Trade_Mark_Sign (no, I’m not making this up!) is awkward if you need to use it more than once. This is why use clauses tend to get overused (pardon the pun). If you don’t like having to write long names like Ada.Characters.Latin_1.ESC and you want to avoid use clauses you can always use a renaming declaration to give the package a more convenient name:
    package Latin_1 renames Ada.Characters.Latin_1;
and then you can just write ‘Latin_1.ESC’ instead of ‘Ada.Characters.Latin_1.ESC’. A renaming declaration like this just provides you with an extra name for an existing object.

If the individual names within the package are awkward to use you can rename them in the same way:

    TM : Character renames Ada.Characters.Latin_1.Registered_Trade_Mark_Sign;
After this declaration you can just use the name TM whenever you want to refer to Ada.Characters.Latin_1.Registered_Trade_Mark_Sign in your program. TM acquires all the characteristics of the object it renames; in this case, since the object being renamed is a constant, TM is also a constant.

The moral is that you should try to use meaningful names (and, as mentioned in chapter 2, avoid use clauses), and then use renaming declarations where necessary to alleviate the burden if the resulting names get too long for comfort. This is quite a common use for declare blocks; long names can be abbreviated for use within a particular section of the program without making the abbreviation universally accessible.

You can also use renaming declarations to rename procedures and functions; you can also change the names and default values of the parameters if you want. The only requirement is that the number and the types of the parameters (and the type of result in the case of functions) are unchanged. So, if you always want floating point values to be displayed in the minimum possible width to two decimal places with no exponent, you can do this:

    procedure Show (Value : in Float;
                    Fore  : in Field := 1;
                    Aft   : in Field := 2;
                    Exp   : in Field := 0)
                    renames Ada.Float_Text_IO.Put;
This gives you a procedure called Show instead of Put; its first parameter is called Value instead of Item and the default values for the other parameters are different to those for Put itself. Show can be used instead of Put like this:
    Show (5.3);     -- displays "5.30"
                    -- same as Put (5.3, Fore=>1, Aft=>2, Exp=>0);
The one situation where you’re not allowed to use renaming declarations is with data types. This means that you can’t say ‘type Time renames Ada.Calendar.Day_Duration’, for example. However, you can achieve the same effect by subtyping:
    subtype Time is Ada.Calendar.Day_Duration;
Now Time is a subtype of Day_Duration so it can be used wherever Day_Duration can be used, but we haven’t restricted its range of values. The result is a type called Time which has the same range of values as Day_Duration and which can be used interchangeably with Day_Duration; in other words they are identical, and Time is effectively just another name for Day_Duration. Renaming a type like this may not be completely satisfactory; in the case of enumerated types you will also need to rename the enumeration literals. Enumeration literals behave as if they were parameterless functions, so if the type Day_Of_Week were defined in the package JE.Dates you could rename the type and its enumeration literals like this:
    subtype  Weekday is JE.Dates.Day_Of_Week;
    function Sun return JE.Dates.Day_Of_Week renames JE.Dates.Sun;
    function Mon return JE.Dates.Day_Of_Week renames JE.Dates.Mon;
    function Tue return JE.Dates.Day_Of_Week renames JE.Dates.Tue;
    function Wed return JE.Dates.Day_Of_Week renames JE.Dates.Wed;
    ... and so on
This is quite awkward and long-winded, but fortunately it’s rarely necessary in practice.

Exercises

5.1 Write a program to play a simple guessing game. Define an integer type with a range of values from 1 to 1000 and declare a secret value as a constant of this type, and then give the user ten chances to guess its value. A message should be displayed at the beginning to tell the user what to do. For each unsuccessful guess, the user should be told whether the guess was too low or too high. You will need to keep a count of the number of attempts. The program ends after the user has successfully guessed the secret value or after the tenth unsuccessful attempt. Display a message of congratulations or condolence at the end of the program. Modify the program so that the value to be guessed is chosen at random each time the program is run. You can generate random values of a discrete type X by instantiating the package Ada.Numerics.Discrete_Random for type X:
    package Random_X is new Ada.Numerics.Discrete_Random (X);
    Gen : Random_X.Generator;  -- a random-value generator
You will of course need a with clause for Ada.Numerics.Discrete_Random. The random-value generator Gen can be initialised ready for use by calling the procedure Reset(Gen); you can then generate random values by calling the function Random(Gen), which will produce a new random value of type X from the generator Gen each time you call it.

5.2 Rewrite the date package from the previous chapter so that it includes a set of type declarations for days, months, years and days of the week. Use an enumeration type for the months and for the days of the week. Modify the functions in the package so that they use these types for their parameters and results instead of Integers and rewrite the main program Weekday so that it reads in a day, month and year as values of the appropriate types and displays the corresponding day of the week using input/output packages created from Ada.Text_IO.Integer_IO and Ada.Text_IO.Enumeration_IO as described earlier.

5.3 Write a function which takes a character as its parameter and returns it converted to lower case if it is an upper case letter, or returns it unchanged otherwise. Note that you can convert from upper case to lower case by adding the difference between an 'a' and an 'A', using Character'Pos and Character'Val to convert characters to and from integers.

5.4 Define data types to represent the suit and value of a playing card. Cards have four suits (Clubs, Diamonds, Hearts and Spades) and 13 cards in each suit (Ace, 2 to 10, Jack, Queen and King). Use Ada.Numerics.Discrete_Random as described in exercise 5.1 above to write a program to display three random cards, each of which is different.



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 $