![]() Previous |
![]() Contents |
![]() Next |
What is essential is invisible to the eye.
Antoine de Saint-Exupéry, The Little
Prince
The program developed in the previous chapter works, but its a very poor program all the same. It was designed oh-so-carefully using top-down design techniques; it was incrementally debugged, with each piece being tested and verified before going on to the next stage; it was carefully modularised using a set of procedures which were then encapsulated in a package. But even after all this, its a terrible program.
So why do I say this? Whats the matter with it? After applying all these design techniques, shouldnt we expect the result to be a shining example of program design? The unfortunate truth is that top-down design is a technique with some severe limitations. It doesnt necessarily take into account the need for reusability, portability or maintainability. It assists in producing maintainable programs in so far as you can replace low-level modules with improved versions, but not when the maintenance needs affect the outer levels of the design. It concentrates on the processing of the data and doesnt pay enough attention to the data itself.
Consider the following maintenance scenarios:
In all of these cases youll end up more or less rewriting the entire program. You need to do more than just fiddle about with low-level details in these situations. In the first case you suffer from the fact that there is a single diary buried in the package. Youll need to move the declaration into the package specification where it is visible to the rest of the program; youll need to rejig the main program to provide extra commands for opening and closing and selecting different diaries; and youll have to pass the selected diary as a parameter to each of the procedures in the package. There wont be any part of the program which escapes the effects of these changes. In the second case the input/output in the main program (displaying the menu and getting the users response) will need changing, but so will the input/output in the package procedures (getting the details of a new appointment, selecting the appointment to be deleted, and so on). In the third case the package wont be reusable in the context of an electronic mail program since it does input and output to the screen and cant accept an appointment from another source. Youd have to add the ability to pass appointments around as parameters to the procedures in the package, which implies a complete rewrite. In the final case all the places where the components of dates are accessed will need rewriting since those components will no longer exist.
In other words, the program is inflexible in its present state. What top-down design as practised in the previous chapter gives you is a workable but brittle program. It will do exactly what you originally wanted, but what it wont necessarily do is adapt easily to changing requirements. None of these maintenance scenarios could have been anticipated at the time of the original development, but maintenance requirements are always going to be unpredictable unless you have an extremely high-class crystal ball in your desk drawer.
However, all is not lost. If you bear in mind the need for maintenance and reuse as you design, you can come up with designs that are a lot less naïve than the one in the previous chapter. The maintenance problems arise largely from the fact that the design concentrated on defining the processing required, and the design of the data structures involved was treated as a subsidiary problem of lesser importance. What is needed is a more data-centred approach. The representation of dates, appointments and so on used record types as containers for a collection of simpler types. Record types have a very limited repertoire of standard operations (assignment and equality testing only) unlike scalar types like Integer which have a rich set of predefined operations. The program just used standard operations to manipulate the record components rather than defining a set of procedures and functions to provide a range of operations for the record types themselves. What is really needed is a set of operations on appointments, dates and so on so that these types can be dealt with as entities in their own right rather than as containers for simpler types. We want to have a set of operations available for dates rather than having to deal with the day, month and year components of dates.
How will this help? Consider the final maintenance scenario described above. The program in its present form relies heavily on the internal representation of dates. If Date_Type were defined in a package of its own which provided a function Day which returned the value of the day component, it would be a relatively simple matter to change the way dates are represented as long as the program using the package always used the function to access the day component rather than accessing it directly. Functions like Day would need rewriting to accommodate the new representation, but this would just be a change to the package body. Programs using the package would not need to be changed. And, as long the package provided a sufficiently rich set of operations on dates, it would be usable in any program which needed to operate on dates. What we would end up with is an abstract data type (ADT) whose operations would allow us to manipulate dates without worrying about the internal representation of the type. An abstract data type should be like a black box whose internal workings are invisible to the user. If you think about it, this is what happens with types like Integer. We dont care how Integers are represented inside the computer; all we care about is that we can do addition and subtraction and so on. What we want to do is to elevate Dates to the point that they are indistinguishable from any of the built-in data types like Integer. We should just be able to use them as if they too were built in to the language.
Packages are the key to reuse in Ada; they allow you to take a clientserver approach to program design. A package acts as a server which provides a set of services to its clients (programs or other packages). Packages are deliberately split into a specification part and a body part; this is in the interests of abstraction as described above. The specification tells you what services the package provides and the body provides the implementation of those services. Only the specification is accessible to clients of the package so that the implementation details are hidden from the clients. The specification can be viewed as a contract with the package clients; it tells the clients what services the package will provide, but not how it will provide them; it is the interface between the contents of the package and its clients. The more general the range of services provided, the more chance that the package will be able to be used and reused in a variety of different situations. This also implies that the package shouldnt assume anything about the properties of its clients, since this will reduce the range of possible clients to those that conform to the packages expectations.
Changes to the package body will not involve any changes to the clients, since the package body is not part of the packages contractual requirements (i.e. it isnt visible to the packages clients). Changes to the specification will involve changes to the clients as well, and since a package may be used by many different programs this can be expensive. Careful design is therefore required to reduce the risk that the package specification might need changing. Here are some important principles that should guide you when you design packages (which I deliberately ignored in the previous chapter!):
Hiding the data structure in the diary avoided revealing its implementation details, so this is apparently in accordance with the first of these three principles. However, its a bad idea as you end up with a single data structure which is referenced from many different places and it becomes more and more difficult to untangle the program from the single instance of the data structure as the program grows. If at some point you want more than one instance of the data structure (as in the first maintenance scenario above) youre in real trouble. Heres one more design principle to round things off:
Rather than saying to yourself I am writing a program to manage an electronic diary you should say I am developing an electronic diary data type; the program is then almost an afterthought, a particular client of the package that contains the data type and the operations it supports. Writing the program may help to identify the operations that a diary should support, but the diary should be the focus of attention rather than the program that uses it. That way there wont be any problems if the program then needs to be changed to handle multiple diaries.
In other words, a package should in most cases be a repository for one or more data types and a set of client-neutral operations on those data types. By client-neutral I mean that the operations shouldnt assume anything about the clients which might use them, so they shouldnt try to handle errors or do input/output, both of which are very client-specific activities. Packages should just provide services to clients that need them without assuming any knowledge about who those clients might be. Occams razor should be applied to package design: a package should provide those operations that are absolutely necessary but nothing more than is absolutely necessary.
Chapter 4 showed a small package called JE.Dates which was further revised in chapter 6. Here is the revised package specification from chapter 6, unchanged except for the function names:
package JE.Dates is subtype Day_Type is Integer range 1..31; type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); subtype Year_Type is Integer range 1901..2099; type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat); type Date_Type is record Day : Day_Type; Month : Month_Type; Year : Year_Type; end record; function Weekday (Date : Date_Type) return Weekday_Type; function Valid (Date : Date_Type) return Boolean; end JE.Dates;
As discussed above, there is a fatal flaw in the package design as it stands. Clients which use the package just access the internal representation of Date_Type in order to extract the components of a date. Clients which directly access the components will need rewriting if the details of the data structure need to be changed at a later date. What is needed is a way of preventing direct access to the components of Date_Type. To solve this problem, Ada allows us to define private types.
A private type is one whose name is given in a package specification but whose internal workings are kept private. Only the package body is allowed to know about the internal representation, so that the body can use this information to implement the operations declared in the package specification. The only standard operations you can perform on values of a private type are assignment (:=) and testing for equality (= and /=). Heres how you declare a private type in a package specification:
type Date_Type is private;
This lets you declare as many variables of type Date_Type as you like, but (except in the package body) the only things you can do with those variables is to assign them to each other using :=, compare them for equality and inequality using = and /=, and use the operations declared in the package specification to manipulate them.
At some point, of course, you do have to reveal the internal workings of the type. The package body has to know how the type is implemented and the compiler has to know how much space to allocate in memory when a variable of that type is declared. This is done by placing a section headed private at the end of the package specification which divides it into two parts: a visible part which is visible to users of the package and a private part which is only accessible from within the package body.
package JE.Dates is -- Visible part of package subtype Day_Type is Integer range 1..31; type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); subtype Year_Type is Integer range 1901..2099; type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat); type Date_Type is private; function Weekday (Date : Date_Type) return Weekday_Type; function Valid (Date : Date_Type) return Boolean; private -- visible part ends here -- Private part of package type Date_Type is record Day : Day_Type; Month : Month_Type; Year : Year_Type; end record; end JE.Dates;
It may seem strange to put the private information in the package specification rather than in the body, but the reason for this is that the compiler needs to know how much space to allocate when compiling programs that use the package (since the compiler only looks at the package specification in these situations). You may be able to look at the private information in the file containing the package specification but you arent allowed to make any use of it in your programs. The compiler will refuse to let programs which use this package refer to anything defined in the packages private part. The only place where you can make use of the information in the private part of a package is in the package body, or in the bodies of any child packages (which are effectively treated as extensions to the parent package).
The great advantage of using private types is that it prevents anyone accessing the data structures directly in order to bypass the operations that your package provides. It also ensures that if you change the information in the private part during maintenance you can guarantee that only the package body (and the bodies of any of its child packages which use the private information) will need changing to match, since the package body is the only place where the private part is accessible. Package clients will need to be recompiled, but as long as there havent been any changes to the visible part of the package specification (or the behaviour it implies) you can guarantee that no changes to the client code will be needed.
Of course, it is now impossible for clients of the package to do anything with dates except assign them to each other, test them for equality, test that they are valid and find out what day of the week they fall on. Since theres no access to the components of a date its impossible to construct a date with a particular value or find out what day, month or year it is. What we need to do is to provide some extra functions to give clients these capabilities without revealing anything more about the internal structure of Date_Type. First we need a set of accessor functions to access the components of a date:
function Day (Date : Date_Type) return Day_Type; function Month (Date : Date_Type) return Month_Type; function Year (Date : Date_Type) return Year_Type;
We also need a constructor function to construct a date from its components:
function Date (Day : Day_Type; Month : Month_Type; Year : Year_Type) return Date_Type;
The constructor function is the only way to create dates, so we dont need to worry about possibilities like the month of a date being changed independently of the rest of it. This makes using dates much safer. In fact, since we can check dates for validity when they are first created by the constructor, the only place that a validity check is needed is inside the function Date itself, which means that Valid doesnt need to be made visible to clients. Date can just raise an exception if its parameters dont represent a valid date, so we need an exception declaration in the visible part of the package:
Date_Error : exception; -- can be raised by Date
This is much more like a built-in type like Integer which doesnt rely on users calling a Valid function to check that Integer values are correct; instead, automatic error checking is done behind the scenes and an exception is raised whenever an error is detected. By adopting the same approach for Date_Type we start to make it look much more like a built-in type.
After these changes, the package specification so far looks like this:
package JE.Dates is -- Visible part of package subtype Day_Type is Integer range 1..31; type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); subtype Year_Type is Integer range 1901..2099; type Weekday_Type is (Sun, Mon, Tue, Wed, Thu, Fri, Sat); type Date_Type is private; -- Accessor functions function Day (Date : Date_Type) return Day_Type; function Month (Date : Date_Type) return Month_Type; function Year (Date : Date_Type) return Year_Type; function Weekday (Date : Date_Type) return Weekday_Type; -- Constructor function function Date (Day : Day_Type; Month : Month_Type; Year : Year_Type) return Date_Type; -- Exception for error reporting Date_Error : exception; -- can be raised by Date private -- visible part ends here -- Private part of package type Date_Type is record Day : Day_Type; Month : Month_Type; Year : Year_Type; end record; end JE.Dates;
Notice that Weekday is in fact just another accessor function, although it has to do a bit more work than the other ones.
You can also define a child package or child subprogram to be private, in which case the entire package is private:
private package JE.Implementation_Details is ... end JE.Implementation_Details;
The only place a private child can be accessed is from the body of its parent package or from the body of another child of the parent package. You cant access it from the specification of a non-private package at all. You can think of a private child as an extension of the private part of the parent package; its completely inaccessible to external clients. Private children can be useful for providing operations which will be shared by the implementation of different children of a parent package.
What a private type declaration does is to give you two separate views of the same type, according to where you are standing. The visible part of the package gives you a partial view of the type by saying its private; this means that clients of the package see a type for which assignment and equality tests are allowed, but no more. The private part of the package provides the full view of the type; this provides more information than the partial view but can only be seen from the private part of the package specification or the package body. Child packages are treated as extensions of the parent package, so the full view of a type is also visible from the private part of a child package specification or from a child package body (or from anywhere within a private child).
The basic rule in Ada is that the partial view should never provide more capabilities than the full view allows. Limited types (see chapter 6) provide an excellent illustration of this. A type which is declared as limited in the private part of the package (the full view) must be declared limited private in the visible part (the partial view):
package Some_Package is type Some_Type is limited private; ... private type Some_Type is limited record ... end record; end Some_Package;
If you were allowed to declare Some_Type as private in the visible part of this package, this would mean that assignment would not be allowed if you had access to the full view but would be allowed if you only had access to the partial view, since non-limited private types allow assignment. However, you can declare a limited private type in the partial view which is non-limited in the full view, since the declaration in the visible part only restricts access in the partial view:
package Some_Package is type Some_Type is limited private; -- assignment not allowed ... private type Some_Type is record ... end record; -- assignment allowed end Some_Package;
Package clients which only have access to the partial view cannot perform assignment or equality testing since they see Some_Type as a limited type, but anywhere which has access to the full view is allowed to do these things.
The idea of different parts of a program having different views of the same type is an important principle which makes the type restrictions associated with private types much easier to understand; you will meet it again in connection with generic type parameters and tagged types.
Sometimes you might want to make a constant of a private type available to the user. For example, you might want to provide a constant representing an invalid date so that users of the date package can use this as an unknown date value. The package doesnt provide any way to construct an invalid date; any attempt to do so will just raise a Date_Error exception. The only way it can be done is to use a knowledge of the internal structure of a date. If you want the value to be available to clients of the package (who cant see the internal structure of Date_Type) you have to define a constant in the visible part whose full definition is deferred to the private part where the full view of the type is available:
package JE.Dates is type Date_Type is private; Invalid_Date : constant Date_Type; -- deferred constant ... private type Date_Type is record Day : Day_Type; Month : Month_Type; Year : Year_Type; end record; Invalid_Date : constant Date_Type := (31,Feb,1901); -- full declaration end JE.Dates;
The deferred declaration of Invalid_Date tells clients that there is a constant called Invalid_Date and that its a Date_Type, but it does this without revealing anything about the internal structure of Date_Type. The full declaration of Invalid_Date is given in the private part of the package after the full declaration of Date_Type has been given. This is the only situation in Ada where you are allowed to omit the value of a constant in a constant declaration, and the omission can only be a temporary one; the full declaration must be given in the private part of the package.
Now lets look at the body for JE.Dates. Heres an outline generated directly from the specification:
package body JE.Dates is function Day (Date : Date_Type) return Day_Type is ... end Day; function Month (Date : Date_Type) return Month_Type is ... end Month; function Year (Date : Date_Type) return Year_Type is ... end Year; function Weekday (Date : Date_Type) return Weekday_Type is ... end Weekday; function Date (Day : Day_Type; Month : Month_Type; Year : Year_Type) return Date_Type is ... end Date; end JE.Dates;
The function Valid will still be needed in the package body even if it isnt required in the specification since Date will need to use it to check if a date is valid. Valid can be made into a procedure which Ill call Validate; all it needs to do is to raise a Date_Error if its parameters dont form a valid date. It can be made much simpler than the Valid function from chapter 4 thanks to the fact that Day_Type, Month_Type and Year_Type ensure that the parameters to Date are at least in range, so the only thing it needs to check is that the value of Day isnt higher than the maximum allowed by Month and Year. It can also be amended to take a Date_Type parameter instead of three separate parameters. Here is the definition of Validate, which should be inserted at the start of the package body above:
procedure Validate (Date : in Date_Type) is begin case Date.Month is when Apr | Jun | Sep | Nov => if Date.Day > 30 then raise Date_Error; end if; when Feb => if (Date.Year mod 4 = 0 and Date.Day > 29) or (Date.Year mod 4 /= 0 and Date.Day > 28) then raise Date_Error; end if; when others => null; end case; end Validate;
Date is now quite simple:
function Date (Day : Day_Type; Month : Month_Type; Year : Year_Type) return Date_Type is D : Date_Type := (Day, Month, Year); begin Validate (D); return D; end Date;
Most of the accessor functions are even simpler:
function Day (Date : Date_Type) return Day_Type is begin return Date.Day; end Day; function Month (Date : Date_Type) return Month_Type is begin return Date.Month; end Month; function Year (Date : Date_Type) return Year_Type is begin return Date.Year; end Year;
Weekday uses Zellers Congruence as described in chapter 4:
function Weekday (Date : Date_Type) return Weekday_Type is D : Integer := Date.Day; M : Integer := Month_Type'Pos(Date.Month) + 1; Y : Integer := Date.Year; C : Integer; begin if M < 3 then Y := Y - 1; M := M + 10; else M := M - 2; end if; C := Y / 100; -- first two digits of Year Y := Y mod 100; -- last two digits of Year return Weekday_Type'Val(((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7); end Weekday;
This just extracts the components of Date into a set of Integers, evaluates Zellers Congruence and then uses Weekday_Type'Val to convert the result (0 to 6) into the corresponding Weekday_Type value. Thanks to the order in which the values of Weekday_Type were declared this will automatically give the correct result (0 = Sun, 1 = Mon and so on). The Month component is converted using Month_Type'Pos; this gives a value between 0 and 11 so the result is adjusted to the range 1 to 12 by adding 1 to it.
There are lots of other operations on dates we could add to round off this package. We could add a set of functions for doing date arithmetic: adding a number of days to a date to get a new date, subtracting a number of days from a date, subtracting two dates to find out the number of days between them, comparing dates to see which is the larger, and so on and so forth. Input/output operations (Get and Put) are another possibility, but these are worth avoiding for at least two reasons: firstly, they would only be of any use to a program with a text-mode interface, and secondly even in text-mode programs they would restrict the formats which could be used for entering dates and displaying them. Since the conventions used for representing dates vary widely from one country to another, this would make any program which actually used the Get and Put procedures provided by a date handling package unusable in any other country.
As mentioned earlier, Occams razor should be used whenever you start to think of clever new things to put into a package. The operations mentioned above all require access to the private part of the package; they could be implemented outside the package using the accessor functions Day and Month and Year, but if the representation of Date_Type were ever changed to a Julian date it would be far easier to add a number of days to a Julian date directly instead of laboriously converting a Julian date into a day, month and year and then performing a much more complex process of addition. Similar arguments apply to comparison functions; Julian dates are easier to compare than dates composed of a day, a month and a year. This means that it is well worth providing these as part of the package itself. However, what about an operation to find when the next Monday (or any other day) after a given date will be? This isnt worth putting in a package like this: firstly, its a very specialised operation that few programs will need, and secondly it can be implemented by adding 1 to a date up to a maximum of seven times (or by finding out what day of the week it is, calculating how many days it is from that day to the following Monday and then adding that number of days to the date). Putting this rarely used and relatively simple function into the package simply increases the bulk of the package to little effect. In general, operations like this are not worth building in as the costs outweigh the benefits. If they do turn out to be useful you can always define them in a child package to avoid bulking out the parent package.
Addition and subtraction could be done with functions declared like this:
function Add (Date : Date_Type; Days : Integer) return Date_Type; function Sub (Date : Date_Type; Days : Integer) return Date_Type; function Sub (First, Second : Date_Type) return Integer;
Thus to find out how many days there are from Now until Christmas you could use Sub (Christmas,Now) and to find out what the date is a week from Now you could use Add (Now,7).
Note that the name Sub is overloaded in the two declarations above. You can use the same name for different subprograms provided that the compiler can work out which one youre referring to. Procedures and functions can always be distinguished since procedure calls are statements whereas function calls occur in expressions. You can also tell subprograms apart if they have different numbers of parameters or different parameter types (as with the two versions of Sub above). Functions can be distinguished by their return types, so you can have several functions with the same name and identical parameter types provided that they return different types of result.
Function calls are a very awkward notation compared to the arithmetic operations for standard types such as integers; if you were dealing with Integers instead of Date_Types you would write the above function calls as Now+7 and ChristmasNow respectively. Using function calls is far less readable than using the equivalent operators and adds to the number of things you have to remember about the data types youre dealing with. Overloading function names (as is done with Sub above) can reduce the burden a little, but it is still awkward and makes user-defined types visibly different from other types.
In fact, operators in Ada are just functions with special names. Writing 2 + 2 is actually equivalent to writing "+"(2,2). The name of the function corresponding to an operator is just the name of the operator enclosed in double quotes. Ada allows you to write functions which overload existing operators; all you have to do is to write a function whose name is the name of the operator enclosed in double quotes. The function must also have the correct number of parameters, which in the case of "<" is two; in the case of the "abs" or "not" operators you would only have one parameter, and in the case of "+" or "" you can define functions with one parameter (the unary version of the operator) or two parameters (the binary version of the operator) or both. Also, you arent allowed to provide default values for the parameters. Operator overloading means that instead of declaring functions called Add and Sub like the ones above, you can declare operators like this:
function "+" (Left : Date_Type; Right : Integer) return Date_Type; function "-" (Left : Date_Type; Right : Integer) return Date_Type; function "-" (Left, Right : Date_Type) return Integer;
Overloading "+" and "" like this means that it is now possible to write expressions like Now+7 or ChristmasNow.
All the operators in Ada can be overloaded except for in, not in, and then and or else which are not normal operators. There are also restrictions on overloading the inequality operator "/=" which Ill explain in a moment. The names of the operators that you can overload are therefore as follows:
+ - * / ** rem mod abs = /= < > <= >= not and or xor &
You cannot invent your own operators (e.g. you cannot invent a percentage operator called "%"); you are also unable to alter the precedence of operators, so that "*" will always be applied before "+". In the case of "=", if it produces a Boolean result then "/=" is automatically defined to produce the opposite result and you are not allowed to redefine "/=" yourself. You can only redefine "/=" if it produces a result of some other type than Boolean; for example, if you have a fuzzy logic type like this:
type Fuzzy is (No, Maybe, Yes);
then you can define "=" to compare two values and return a Fuzzy result, but you wont automatically get a corresponding version of "/=". Youll have to define "/=" yourself in this case.
There is nothing special or magical about overloading operators; you can define a function named after an operator whenever you like. It doesnt have to be in a package specification, but this is normally where you want to do it, in connection with a type which is declared in the same package. The only time you cant use a name like "+" for a function is if you want to compile it as a free-standing library unit; overloaded operators must be enclosed within library units (packages or subprograms) which have normal alphanumeric names.
We can also overload "<", ">", "<=" and ">=" to compare pairs of dates:
function "<" (Left, Right : Date_Type) return Boolean; function ">" (Left, Right : Date_Type) return Boolean; function "<=" (Left, Right : Date_Type) return Boolean; function ">=" (Left, Right : Date_Type) return Boolean;
Notice that it isnt necessary to overload "=" and "/="; private types already have these operators predefined and they will give the expected result. Given the definition of "<" above, we can now write things like this:
if Now < Date(25,Dec,1995) then ...
Heres how "<" for dates could be implemented:
function "<" (Left, Right : Date_Type) return Boolean is begin if Left.Year /= Right.Year then return Left.Year < Right.Year; elsif Left.Month /= Right.Month then return Left.Month < Right.Month; else return Left.Day < Right.Day; end if; end "<";
The version of "<" above is defined in terms of the versions of "<" predefined for Day_Type, Month_Type and Year_Type. There is no problem with this; the compiler always knows which version of "<" you are referring to by the types of the operands you use it with. This is exactly the same as having a number of procedures called Put with different types of parameters; the operands on either side of an operator are its parameters, and the compiler uses these to figure out whats happening in just the same way as it uses the type of parameter supplied for Put to figure out which version of Put youre talking about.
Heres how the other comparison operators can be implemented:
function ">" (Left, Right : Date_Type) return Boolean is begin return Right < Left; end ">"; function "<=" (Left, Right : Date_Type) return Boolean is begin return not (Left > Right); end "<="; function ">=" (Left, Right : Date_Type) return Boolean is begin return not (Left < Right); end ">=";
Since A>B means the same as B<A, we can define the ">" operator using the "<" operator defined above; similarly A<=B is True if A>B is False and A>=B is True if A<B is False. This makes the definitions nice and easy to write, and if the representation of Date_Type ever changes youll only ever need to change the definition of "<" in order to change all of them.
One of the problems with defining your own operators is that if you dont provide a use clause for the package that theyre in, you cant use them in the normal way; operator names like "+" wont be accessible and youll have to use fully qualified names like JE.Dates."+" instead. This means that in the absence of a use clause for JE.Dates youd have to write this instead of Tomorrow := Weekday (Today + 1):
Tomorrow := JE.Dates.Weekday (JE.Dates."+" (Today, 1));
This is seriously ugly, and in the absence of any other solution it would more or less force you to write use clauses for any packages declaring data types with overloaded operators. One solution would be to use renaming declarations for the operators you needed to use, like this:
function "+" (Left : JE.Dates.Date_Type; Right : Integer) return JE.Dates.Date_Type renames JE.Dates."+";
In situations where there are lots of different operators, this is still extremely awkward. Fortunately theres a variant of the use clause designed to help in exactly this situation:
use type JE.Dates.Date_Type;
This gives you access to the operators of the specified type from the specified package (Date_Type from JE.Dates in this case) but not anything else, so with a use type clause youd be able to write this:
Tomorrow := JE.Dates.Weekday (Today + 1);
Note that although operator names like "+" are directly accessible thanks to the use type clause, other names like Weekday are still not accessible and must still be fully qualified with the package name.
It is only ever a good idea to overload operators when the meaning will be absolutely obvious to anyone reading a program which uses them. There is nothing to prevent you from doing silly things like defining "+" to do subtraction or multiplication (or anything else for that matter); however, common sense should tell you never to use "+" as the name of a function which is not doing something addition-like. It is possible to be too cute for your own good; here is an example which shows how "" can be redefined as a way of constructing dates:
subtype Day_Type is Integer range 1..31; type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); subtype Year_Type is Integer range 1901..2099; type Date_Type is record Day : Day_Type; Month : Month_Type; Year : Year_Type; end record; type Day_And_Month is record Day : Day_Type; Month : Month_Type; end record; function "-" (Left : Day_Type; Right : Month_Type) return Day_And_Month is Result : Day_And_Month := (Day => Left, Month => Right); begin return Result; end "-"; function "-" (Left : Day_And_Month; Right : Year_Type) return Date_Type is Result : Date_Type := (Day => Left.Day, Month => Left.Month, Year => Right); begin return Result; end "-";
This means that you can write 25Dec1995 to mean Christmas Day, 1995: 25Dec gives a Day_And_Month value consisting of a Day component set to 25 and a Month component set to Dec; this is then combined with 1995 using the second version of "" above to give a complete date. I do not recommend doing this! It may look nice but it is using "" to do an operation which has no relationship at all to subtraction and this is a possible source of confusion; in another context where Dec is an Integer, the meaning of this expression will be completely different.
Of course, theres not a lot of point in defining your own date handling package when the standard package Ada.Calendar already does practically everything you want. Heres the full specification of Ada.Calendar:
package Ada.Calendar is type Time is private; subtype Year_Number is Integer range 1901 .. 2099; subtype Month_Number is Integer range 1 .. 12; subtype Day_Number is Integer range 1 .. 31; subtype Day_Duration is Duration range 0.0 .. 86_400.0; function Clock return Time; function Year (Date : Time) return Year_Number; function Month (Date : Time) return Month_Number; function Day (Date : Time) return Day_Number; function Seconds (Date : Time) return Day_Duration; procedure Split (Date : in Time; Year : out Year_Number; Month : out Month_Number; Day : out Day_Number; Seconds : out Day_Duration); function Time_Of (Year : Year_Number; Month : Month_Number; Day : Day_Number; Seconds : Day_Duration := 0.0) return Time; function "+" (Left : Time; Right : Duration) return Time; function "+" (Left : Duration; Right : Time) return Time; function "-" (Left : Time; Right : Duration) return Time; function "-" (Left : Time; Right : Time) return Duration; function "<" (Left, Right : Time) return Boolean; function "<="(Left, Right : Time) return Boolean; function ">" (Left, Right : Time) return Boolean; function ">="(Left, Right : Time) return Boolean; Time_Error : exception; private ... -- not specified by the language end Ada.Calendar;
As you can see, this defines a private type Time which represents a date and time together with a number of supporting scalar types. It defines a set of accessor functions (Year, Month, Day, Seconds) and a constructor function (Time_Of). There is a deconstructor procedure called Split which splits a Time into its component parts. There are also arithmetic operators and comparison operators and an exception Time_Error for reporting any errors that are detected by the operations in the package. The only problem with the package is that the time of day is expressed as a number of seconds rather than as an hour, minute and seconds, and the month is represented by an integer instead of an enumerated type. These could be provided in a separate package, but the disadvantage of this approach is that users will end up having to use both Ada.Calendar and the new package together. This means that the users would have to keep track of which operations belonged to which packages, or provide use clauses for both. A simple solution is to write a wrapper package which provides the extra operations as well as providing the existing operations by renaming. Although the result is not very pretty, its a much easier solution from the users point of view since all the operations and data types are then provided in a single package, and its attractive (if ugly!) from an implementation point of view since it minimises the amount of new code that needs to be written. Heres the result:
with Ada.Calendar; package JE.Times is subtype Time_Type is Ada.Calendar.Time; subtype Year_Type is Ada.Calendar.Year_Number; type Month_Type is (Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec); subtype Day_Type is Ada.Calendar.Day_Number; subtype Hour_Type is Integer range 0..23; subtype Minute_Type is Integer range 0..59; subtype Second_Type is Integer range 0..59; subtype Day_Duration is Ada.Calendar.Day_Duration; function Clock return Ada.Calendar.Time renames Ada.Calendar.Clock; function Interval (Days : Natural := 0; Hours : Natural := 0; Minutes : Natural := 0; Seconds : Natural := 0) return Duration; function Year (Date : Ada.Calendar.Time) return Year_Type renames Ada.Calendar.Year; function Month (Date : Time_Type) return Month_Type; function Day (Date : Ada.Calendar.Time) return Day_Type renames Ada.Calendar.Day; function Hour (Date : Time_Type) return Hour_Type; function Minute (Date : Time_Type) return Minute_Type; function Second (Date : Time_Type) return Second_Type; function Time (Year : Year_Type; Month : Month_Type; Day : Day_Type; Hour : Hour_Type := 0; Minute : Minute_Type := 0; Second : Second_Type := 0) return Time_Type; function "+" (Left : Ada.Calendar.Time; Right : Duration) return Ada.Calendar.Time renames Ada.Calendar."+"; function "+" (Left : Duration; Right : Ada.Calendar.Time) return Ada.Calendar.Time renames Ada.Calendar."+"; function "-" (Left : Ada.Calendar.Time; Right : Duration) return Ada.Calendar.Time renames Ada.Calendar."-"; function "-" (Left : Ada.Calendar.Time; Right : Ada.Calendar.Time) return Duration renames Ada.Calendar."-"; function "<" (Left, Right : Ada.Calendar.Time) return Boolean renames Ada.Calendar."<"; function "<="(Left, Right : Ada.Calendar.Time) return Boolean renames Ada.Calendar."<="; function ">" (Left, Right : Ada.Calendar.Time) return Boolean renames Ada.Calendar.">"; function ">="(Left, Right : Ada.Calendar.Time) return Boolean renames Ada.Calendar.">="; Time_Error : exception renames Ada.Calendar.Time_Error; end JE.Times;
Interval is a new addition to the package which constructs a Duration from a number of days, hours, minutes and seconds. The parameters all have defaults of zero, so that you can define intervals like this:
Interval (Days => 7) -- a week Interval (Hours => 48) -- 2 days Interval (Minutes => 1, Seconds => 30) -- a minute and a half
Apart from the functions Interval, Month, Hour, Minute, Second and Time, everything in this package is just a renaming of the corresponding parts of Ada.Calendar (with subtyping being used for type renaming). However, the renamed subprograms have to use the original type names since the types of the parameters in a renaming declaration must be identical to those in the original subprogram. The extra functions are easy enough to implement:
package body JE.Times is Seconds_Per_Minute : constant := 60; Minutes_Per_Hour : constant := 60; Hours_Per_Day : constant := 24; Seconds_Per_Hour : constant := Minutes_Per_Hour * Seconds_Per_Minute; Seconds_Per_Day : constant := Hours_Per_Day * Seconds_Per_Hour; type Integer_Time is range 0 .. Seconds_Per_Day - 1; function Convert_Time (Time : Day_Duration) return Integer_Time is type Extended_Integer_Time is range Integer_Time'First .. Integer_Time'Last + 1; T : Extended_Integer_Time := Extended_Integer_Time(Time); begin return Integer_Time (T mod Extended_Integer_Time'Last); end Convert_Time; function Interval (Days : Natural := 0; Hours : Natural := 0; Minutes : Natural := 0; Seconds : Natural := 0) return Duration is begin return Duration( Days * Seconds_Per_Day + Hours * Seconds_Per_Hour + Minutes * Seconds_Per_Minute + Seconds ); end Interval; function Month (Date : Ada.Calendar.Time) return Month_Type is begin return Month_Type'Val (Ada.Calendar.Month(Date) - 1); end Month; function Hour (Date : Time_Type) return Hour_Type is S : Ada.Calendar.Day_Duration := Ada.Calendar.Seconds (Date); begin return Hour_Type( Convert_Time(S) / Seconds_Per_Hour ); end Hour; function Minute (Date : Time_Type) return Minute_Type is S : Ada.Calendar.Day_Duration := Ada.Calendar.Seconds (Date); begin return Minute_Type( (Convert_Time(S) / Seconds_Per_Minute) mod Minutes_Per_Hour ); end Minute; function Second (Date : Time_Type) return Second_Type is S : Ada.Calendar.Day_Duration := Ada.Calendar.Seconds (Date); begin return Second_Type( Convert_Time(S) mod Seconds_Per_Minute ); end Second; function Time (Year : Year_Type; Month : Month_Type; Day : Day_Type; Hour : Hour_Type := 0; Minute : Minute_Type := 0; Second : Second_Type := 0) return Time_Type is Seconds : Day_Duration := Day_Duration( Hour * Seconds_Per_Hour + Minute * Seconds_Per_Minute + Second ); begin return Ada.Calendar.Time_Of (Year, Month_Type'Pos(Month) + 1, Day, Seconds); end Time; end JE.Times;
So by reusing whats already available instead of reinventing the wheel, we end up with a fully functional package in a fraction of the time it would have otherwise taken. Theres a moral in this somewhere, Im sure!
Theres one nasty little trap in the package body which Ive carefully avoided. Since real values are converted to integers by rounding, Day_Duration is rounded to an integer in the range 0 to 86400. We actually want a value in the range 0 to 86399, so the result of the rounding needs to be taken modulo 86400. If Id forgotten to do this the program would crash in the last half-second before midnight, but would work perfectly the rest of the time. Bugs like this can be quite hard to detect, since very few people do their debugging at exactly half a second before midnight!
Ive defined the function Convert_Time to do the conversion from Day_Duration to Integer_Time. Ive had to define an internal type called Extended_Integer_Time with a range of 0..86400 rather than 0..86399 to avoid constraint errors when rounding from Day_Duration. The mod operator is then used to produce a result in the range 0..86399 which is then converted to an Integer_Time result. Using a modular type for Integer_Time wouldnt help since values arent forced into range when converting to a modular type; youd still get a constraint error in the last half-second before midnight. If Duration were a floating point type, you could get around this problem by using the 'Truncation attribute (see Appendix C) to do the conversion by truncation instead of rounding. Unfortunately there is no 'Truncation attribute for fixed point types, which appears to be an oversight on the part of the language standards committee; there is no easy way to do fixed point truncation without using an integer type with a wider range than you actually require.
9.1 | Write a package which defines an abstract data type to represent fractions like 1/2 or 2/3. A fraction consists of a numerator (an Integer) which is divided by a denominator (a Positive). Fractions should always be stored in their lowest possible form, so that 2/4 is always reduced to 1/2; you can do this by always dividing the numerator and denominator by their greatest common divisor, using Euclids algorithm (see exercise 4.3) to find the greatest common divisor. Provide a complete set of arithmetic operations between pairs of Fractions (e.g. 1/2 + 1/3) as well as between Fractions and Integers (e.g. 1/2 + 2) and between Integers and Fractions (e.g. 2 + 1/2) as well as a function to convert a rational number to a Float. Note that the "/" operator can be overloaded for use as a constructor, dividing an Integer by an Integer to return a Fraction as its result. |
9.2 | Modify the playing card package from exercise 7.4 to use private types for cards and packs of cards. A card should have a Boolean component called Valid with a default value of False to indicate whether a card variable contains a valid card or not, and a pack should have a Boolean component called Initialised with a default value of False to show whether it has been initialised. All operations on a pack of cards should check the Initialised member and initialise the pack if necessary so that it contains a full set of 52 cards. Note that cards and packs should be limited private types so that it isnt possible to duplicate them by assignment. Provide a Move operation which moves a valid card into an invalid variable, leaving the destination valid and the original card invalid, so that it is impossible to create duplicate cards. It should be an error to move a card into another card which is marked as valid, since this would allow existing cards to be destroyed. You will also need functions to access the suit and value of a card. |
9.3 | One of the remaining shortcomings in the Ada type system is that if a numeric type is declared to represent physical values such as lengths in metres, the inherited multiplication operator will multiply two lengths to produce another length. In reality, multiplying metres by metres gives square metres. Produce a package to represent dimensioned physical quantities by recording the dimensions of mass (in kilograms), length (in metres) and time (in seconds) involved together with the magnitude of the quantity. For example, acceleration is measured in metres per second squared (m s2), so the dimensions are 0 for mass, 1 for length and 2 for time. Provide a set of arithmetic operators for dimensioned quantities. They can only be added or subtracted if the dimensions match (so you cant add density and acceleration); they can be multiplied or divided by multiplying or dividing the magnitudes and adding or subtracting the corresponding dimensions. |
9.4 | Modify the diary program from chapter 8 so that it uses the package JE.Times defined in this chapter, and modify the Load procedure so that appointments with dates prior to todays date (without taking the time into account) are ignored. |
![]() 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.