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 YThus 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 valueThe + 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 xorEvaluation 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 = -2Note 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_DayOperations 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_NumberThe 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 51The 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. 3600These 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 XThese 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) = ThuNote 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 ComputerThis 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 valueThe 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 BThese 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 FalseAnd, 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 interpretationThere 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 onThis 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
package Random_X is new Ada.Numerics.Discrete_Random (X); Gen : Random_X.Generator; -- a random-value generatorYou 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
|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.|
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 $