![]() Previous |
![]() Contents |
![]() Next |
So careful of the type she seems.
Tennyson, In Memoriam
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 youre 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 cant be represented exactly since we dont 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.
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 youve 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 havent 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 theres 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
cant 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
cant be assigned to a Time_Of_Day variable
because the types dont match. The problem
doesnt 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 cant 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; heres 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.
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 theyre just different aspects of the same thing, but we still want the benefits which arise from telling the compiler what were trying to do so that it can check if were doing it right. For example, we want to specify that the right hand operand in an integer exponentiation is non-negative so that we dont 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 isnt
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 dont 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 cant 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.
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_Numbers
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, youll 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.
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 its
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 youll 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);
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.
Its 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.
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 doesnt 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
arent 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, thats
only because its such a short example. If you
compare the if statement above to the one in the
previous example, Im sure youll agree that its
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?
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;
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 arent 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 cant 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 isnt;
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 cant 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; Ill 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
didnt allow). Heres 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;
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.
type Boolean is (False, True);
Boolean plays a special role in Ada; its 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 wont 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. Weve 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
Bs 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 wont be evaluated. This means that the
division by zero wont happen, so a constraint
error wont occur. The right hand side will only
be evaluated if B is non-zero, in which case its
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 beginners 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.
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 dont 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 doesnt 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. Heres the declaration of a data type which represents the operators used in the previous chapters 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.
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 youre not allowed to use
renaming declarations is with data types. This
means that you cant 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 havent 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 its rarely necessary in practice.
| 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 $