Previous

Contents

Next

Chapter 4:
Procedures, functions and packages

All are but parts of one stupendous whole.
— Alexander Pope, An Essay on Man


4.1 Zeller's Congruence
4.2 Declaring procedures
4.3 Declaring functions
4.4 Scope and lifetime
4.5 Separate compilation
4.6 Subprograms as library units
4.7 Packages
4.8 Child packages
Exercises

4.1 Zeller’s Congruence

It’s time to move on to another example, this time to find out what day of the week it is for a given date. We’ll take a date expressed as three integers (day, month and year) and figure out what day it falls on by using a wonderful formula called Zeller’s Congruence. To do this, we need to read in the three integers, evaluate Zeller’s Congruence, and report the result. Here’s a first stab at the program:
    with Ada.Text_IO, Ada.Integer_Text_IO;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day    : Integer;
        Month  : Integer;
        Year   : Integer;
        Result : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        -- Apply Zeller's Congruence
        Put (Result);
    end Weekday;

For the sake of simplicity I’ll assume for now that the user will always type in a valid date. Let’s see how Zeller’s Congruence works first. Here’s the formula:

    Day = ((26M-2)/10 + D + Y + Y/4 + C/4 - 2C) mod 7
Here M is the number of the month, D is the day, Y is the last two digits of the year number and C is the century (the first two digits of the year number). Integer division is used, so that 19/4 = 4 rather than 4.75. The mod operation gives the remainder of an integer division, in this case the remainder from dividing by 7, i.e. a value between 0 and 6. Things are made slightly more complicated by the fact that the months have to be numbered starting with March as month 1; January and February are treated as months 11 and 12 of the previous year. We therefore need to adjust the month and year like this:
    if Month < 3 then
        Year  := Year - 1;       -- subtract 1 from year number
        Month := Month + 10;     -- convert 1 and 2 to 11 and 12
    else
        Month := Month - 2;      -- subtract 2 from month number
    end if;
The result of the formula is a number between 0 and 6, where 0 means Sunday and 6 means Saturday. Let’s see how this works using January 25th 1956 as an example. The value of D is 25 and C is 19. January 1956 counts as month 11 of 1955, so M is 11 and Y is 55. This gives us:
    Day = ((26M-2)/10 + D + Y + Y/4 + C/4 - 2C) mod 7
        = ((26*11-2)/10 + 25 + 55 + 55/4 + 19/4 - 2*19) mod 7
        = (284/10 + 25 + 55 + 13 + 4 - 38) mod 7
        = (28 + 25 + 55 + 13 + 4 - 38) mod 7
        = 87 mod 7
        = 3 (Wednesday).
It will help to introduce an extra variable for the value C since it’s used twice in the formula above. Here’s the latest version of the program:
    with Ada.Text_IO, Ada.Integer_Text_IO;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day     : Integer;
        Month   : Integer;
        Year    : Integer;
        Century : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        if Month < 3 then
            Year  := Year - 1;
            Month := Month + 10;
        else
            Month := Month - 2;
        end if;
        Century := Year / 100;        -- first two digits of Year
        Year    := Year mod 100;      -- last two digits of Year
        Put (((26*Month - 2)/10 + Day + Year + Year/4 + Century/4 - 2*Century) mod 7);
    end Weekday;
The Ada version of Zeller’s Congruence is practically identical to the original except that the single-letter variable names in the original have been replaced by longer names. Also, all multiplication operators must be specified explicitly; while the original formula had 26M in it, a direct Ada equivalent would have to be 26*M. The order of operations is the same as in ordinary algebra; multiplication and divisions are done before additions and subtractions, with parentheses being used to alter the order of evaluation where necessary.

The other thing to notice is that the value of Century is calculated after the if statement. This is because the if statement might change the value of Year; January 2000 is treated as being month 11 of 1999, so the value for Century will be 19 until March 2000. Likewise Year only gets trimmed to its last two digits after the first two digits have been extracted into Century. The order of events is quite important here.


4.2 Declaring procedures

Zeller’s Congruence is clearly the sort of thing that could be useful in a number of different programs. It would therefore be a good idea to make it into a separate procedure which could be called from any program which needs to use it. Procedures are also a good way of breaking long programs up into manageable chunks; as a general rule of thumb, any procedure that won’t fit on a single printed page is probably too long to read and understand easily and you should consider breaking it up into smaller procedures.

The first thing to do is to decide what inputs it requires and what outputs it produces. There are three inputs: the day, the month and the year. There is a single output, which is a value between 0 and 6. This is enough information to produce a procedure specification:

    procedure Day_Of (Day, Month, Year : in Integer;
                      Result           : out Integer);
What we have here is the specification for a procedure called Day_Of (since Zeller would be a bit cryptic) which has four parameters. Three of them (Day, Month and Year) are Integer inputs, and the fourth (Result) is an Integer output. Designing a specification is always a good way to start writing a procedure, but what we actually need here is a procedure body (i.e. including the implementation details between is and end) rather than just a specification: specifications are generally only used in connection with packages. Writing the body is fairly straightforward; the necessary code can be extracted from the previous program:
    procedure Day_Of (Day, Month, Year : in Integer;
                      Result           : out Integer) is
        M : Integer := Month;
        Y : Integer := 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
        Result := ((26*M - 2)/10 + Day + Y + Y/4 + C/4 - 2*C) mod 7;
    end Day_Of;
Notice that the parameter declarations look just like variable declarations, and that they can in fact be treated as if they were variables inside the procedure body. When the procedure is called, the actual values specified for the input parameters are copied into the corresponding parameter ‘variables’ in the procedure. The procedure body is then executed, and it can make use of the input parameters just as if they were ordinary variables except that it can’t alter them. Since procedures can’t alter the value of in parameters, we need to copy Month and Year into ordinary variables that can be altered. Output parameters behave just like uninitialised variables; you have to set them to some value before you can use them. At the end of the procedure, the values of the output parameter ‘variables’ will be copied into the variables which were used for the output parameters in the procedure call. In this case, the value of Result will be copied into the variable which was supplied for the Result parameter.

As well as in and out, we can specify parameters as in out meaning that they act as both inputs and outputs. Like an out parameter you have to supply a variable name for an in out parameter when you call the procedure so that the procedure has somewhere to store its output; the difference is that the original value of the variable before the procedure call is passed into the procedure as an input. Inside a procedure, an out parameter behaves like an uninitialised variable (you don’t get the original value of the variable supplied when the procedure is called) but an in out parameter behaves like an initialised variable; it is initialised to the value of the variable supplied as the actual parameter when the procedure is called.

Here’s how you could call the procedure to find out the day of the week for January 25th 1956 and put the result in a variable called R:

    Day_Of (25, 1, 1956, R);
This will copy the values 25, 1 and 1956 into the three input parameters Day, Month and Year and then execute the procedure. At the end of the procedure the value of the output parameter Result will be copied into the variable R.

You could also use the parameter names in the procedure call for the sake of readability:

    Day_Of (Day => 25, Month => 1, Year => 1956, Result => R);
or if you wanted to you could use a mixture of the two styles:
    Day_Of (25, 1, Year => 1956, Result => R);
If you mix the two styles in this way the named parameters must come after any without names. One of the advantages of using named parameters is that you can specify them in any order and the compiler will be able to arrange them into the correct order automatically:
    Day_Of (Result => R, Month => 1, Day => 25, Year => 1956);

4.3 Declaring functions

Using a procedure in this case means that two steps are needed to display the procedure’s result. Here’s what we have to do:
    Day_Of (Day, Month, Year, R);
    Put (R);

We call Day_Of and put its result in a variable R. Then we have to pass R as a parameter to Put. Two statements are needed, not to mention the extra intermediate variable R. An alternative solution would be to define Day_Of as a function. A function is just like a procedure except that it evaluates to a result that can be used as part of an expression; for example, a function call could be used as the value for an input parameter in a procedure call (or a call to another function), or as part of an expression on the right hand side of an assignment statement. A function is restricted to producing a single result, and only in parameters are allowed. Functions and procedures are collectively referred to as subprograms.

Since there is a single result produced by the Day_Of procedure it would be easy to turn it into a function. This would allow us to write something like this:

    Put ( Day_Of(Day, Month, Year) );
where the result of the function call is used directly as the parameter to Put. This eliminates the extra statement and the extra variable.

Here’s how you could rewrite the procedure specification above as a function specification:

    function Day_Of (Day, Month, Year : Integer) return Integer;
Instead of an ‘out Integer’ parameter to store the result in, you specify ‘return Integer’ after the parameter list to show that the function returns an Integer as its result. Note that you only have to say ‘Integer’ instead of ‘in Integer’ in the function parameter declaration, since you aren’t allowed out or in out parameters to functions. Only in parameters are allowed, and as a consequence you don’t need to specify in.

Here’s how the function would be implemented:

    function Day_Of (Day, Month, Year : Integer) return Integer is
        M : Integer := Month;
        Y : Integer := 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 ((26*M - 2)/10 + Day + Y + Y/4 + C/4 - 2*C) mod 7;
    end Day_Of;
The return statement specifies an expression whose value is the value to be returned as the result of the function. When you execute a return statement, you evaluate the expression and then immediately exit the function. For example, if you called the function like this:
    I := Day_Of (X, Y, Z);
the values of X, Y and Z would be copied into the parameters Day, Month and Year. The body of the function would then be executed up to the return statement; the value of the expression in the return statement would then be returned from the function and assigned to the variable I.

There is also a form of return statement for use in procedures:

    return;
All this does is to exit immediately from the procedure it is used in and return to the point where the procedure was called from.


4.4 Scope and lifetime

Now all we have to do is to integrate this function into the program. There are several ways to do this. The first is to put the function declaration directly into the declaration part of the main program:
    with Ada.Text_IO, Ada.Integer_Text_IO;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year : Integer;                     -- 1
        function Day_Of (Day, Month, Year : Integer)
                         return Integer is              -- 2
            D : Integer := Day;                         -- 3
            M : Integer := Month;
            Y : Integer := 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 ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
        end Day_Of;                                     -- 4

    begin        -- main program
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        Put ( Day_Of(Day,Month,Year) );                 -- 5
    end Weekday;

At this point it is worth mentioning something about the declarations of the variables inside the Day_Of function. When a variable declaration is executed (or elaborated, to use the correct technical term) some space in memory is reserved for that variable and initialised if the declaration specifies an initial value. The variable exists until the end of the begin ... end block which follows it, and the space allocated to it is then reclaimed so that it can be used elsewhere if necessary. This means that when Day_Of is called at line 5 the variable D inside Day_Of is created when the declaration at line 3 is elaborated. D is then destroyed at the end of Day_Of (line 4) before returning to line 5. The region from the declaration to the end of the block it is defined in is known as the scope of the variable. Since the variable only exists inside its scope, it cannot be accessed from elsewhere (i.e. from another subprogram) since it might not exist at the time; thus D cannot be accessed from the main program since it only exists during the call to Day_Of at line 5. In other words, variables declared in a subprogram are local to that subprogram and are only accessible from inside that subprogram since this is the only time their existence is guaranteed. Parameters are treated the same way; they are effectively local variables which are created during the subprogram call and destroyed when the call is complete.

Variables in different scopes can have the same name as each other; this means that you don’t have to worry about avoiding names which are used in other subprograms. Thus there is no confusion about using Day as the name of a variable at line 1 and as the name of a parameter at line 2; the variable called Day at line 1 is local to the main program while the parameter called Day at line 2 is local to Day_Of. When you refer to a name, the most local object of that name is accessed, so that the version of Day being referred to at line 3 is the parameter declared at line 2 whereas the version referred to in line 5 is the variable declared at line 1.

Each time a subprogram is called, the local variables it declares are created. This means that each variable will be reinitialised every time the subprogram is called if its declaration specifies an initial value; if not its value will be unpredictable. There is no guarantee that a particular variable will be allocated the same space in memory each time the subprogram is called, so there is no way of knowing what the contents of the variable will be unless the declaration explicitly specifies an initial value. In particular, variables do not retain their values from one subprogram call to the next. D, M and Y will be initialised every time Day_Of is called; C will always start off with an unpredictable value. If you do want to retain a value from one call to the next, you have to use a variable declared in the surrounding scope. In this case a variable declared in the main program before Day_Of could be used to hold a value from one call of Day_Of to the next since its scope would be that of the main program; since that scope includes Day_Of it would be accessible inside Day_Of but it would not be destroyed until the end of the main program was reached.

Locality of variables is useful for isolating errors; if during debugging you found that the value of C was wrong you would know that the fault lay somewhere inside Day_Of, since this is the only place where C is accessible. It would be perfectly possible to declare C in the main program instead of inside Day_Of (it would still be accessible inside Day_Of) but you would have to look at the main program as well as Day_Of if its value was found to be incorrect during debugging since its scope would be the whole main program. It would also be wasteful of memory, since space would be allocated for C even when it wasn’t being used. The moral of the story is that variables should always be made as local as possible. If you want to, you can localise declarations even further than the subprogram level by using a declare block:

    declare
        -- declarations local to the declare block
    begin
        -- statements using the local declarations
    end;                -- declarations go out of scope here
If you want to, you can also put an exception handler in a declare block like the one above:
    declare
        -- declarations local to the declare block
    begin
        -- statements using the local declarations
    exception
        -- exception handlers for the statements above
    end;                -- declarations go out of scope here
Although you may not have realised this, you met declare blocks for the first time at the end of the last chapter in connection with exception handling inside loops. The reason you might not have noticed is that the declaration section (everything before begin) was omitted, which is what you have to do if you don’t want any declarations. If there are declarations between declare and begin, they are local to the block. They are created when their declarations are elaborated, they are destroyed at the end of the block, and they are only accessible within the block. As an example, here’s a version of Day_Of where a declare block is used to localise the declaration of C:
    function Day_Of (Day, Month, Year : Integer) return Integer
    is
        M : Integer := Month;
        Y : Integer := Year;
    begin
        if M < 3 then
            Y := Y - 1;
            M := M + 10;
        else
            M := M - 2;
        end if;
        declare
            C : Integer := Y / 100;                     -- 1
        begin
            Y := Y mod 100;
            return ((26*M - 2)/10 + Day + Y + Y/4 + C/4 - 2*C) mod 7;
        end;                                            -- 2
    end Day_Of;                                         -- 3
C is not created until line 1 and destroyed at line 2, whereas M and Y are created at the start of the function and destroyed at line 3.


4.5 Separate compilation

There are several disadvantages to implementing the function inside the main program as described above. One disadvantage is that it makes the program harder to read; the body of the main program is right down at the end and is swamped by the declarations which precede it. Also, if you needed to make a change to Day_Of you would have to recompile the whole thing, not just the function. These problems are easy enough to resolve; Ada allows us to take embedded procedures and functions from a declaration section and compile them separately. All we have to do is to declare the function as being separate. The main program ends up looking like this:
    with Ada.Text_IO, Ada.Integer_Text_IO;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year : Integer;

        function Day_Of (Day, Month, Year : Integer) return Integer
                is separate;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        Put ( Day_Of (Day,Month,Year) );
    end Weekday;
This is a lot clearer. The function is then put in a separate file, compiled separately and linked with the main program. We have to tell the compiler where the function came from by including the line ‘separate (Weekday)’ at the beginning:
    separate (Weekday)
    function Day_Of (Day, Month, Year : Integer) return Integer
    is
        D : Integer := Day;
        M : Integer := Month;
        Y : Integer := 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 ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
    end Day_Of;
This tells the compiler that function Day_Of is a function declared as being separate inside Weekday. The effect is identical to the original version with the function embedded in the declaration section of the main program (in particular, it is still within the scope of the main program), but the main program is now less cluttered and if you have to make any changes to Day_Of, a sensible compiler will only require you to recompile the file containing Day_Of and then relink the main program. The main program shouldn’t need recompiling if it hasn’t been changed. If the main program changes you’ll still have to recompile both the main program and Day_Of in case the changes to the main program had some effect on Day_Of (such as a change to the function declaration).


4.6 Subprograms as library units

A problem with both approaches described above is that Day_Of is declared as being local to Weekday, which means it can’t be accessed from anywhere outside Weekday. This means that it can’t be used by another program without physically duplicating the code for it. Also, Day_Of is able to access anything which is declared before it in the declaration section of Weekday. This means that a coupling exists between Day_Of and Weekday. Any sort of coupling should be avoided if components are going to be reusable; they should be able to be used anywhere, not just in a particular environment.

To get around these problems we could just compile Day_Of as an independent library unit in its own right, just like the procedure Hello which was used as a component of the program Hello_3 in chapter 2. When Day_Of is compiled it gets added to the library and it is then available for use as a component in a larger program. This is only possible if it doesn’t rely on anything which is not a standard built-in part of the language or else provided in a package such as Text_IO that can be accessed using a with clause. In other words, it is restricted to accessing the same things as the main program is. In this case the function doesn’t rely on anything external so there’s no problem:

    function Day_Of (Day, Month, Year : Integer) return Integer
    is
        D : Integer := Day;
        M : Integer := Month;
        Y : Integer := 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 ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
    end Day_Of;
Now we have to specify in the main program that Day_Of is a library unit that we want to refer to, so we need to name it in a with clause:
    with Ada.Text_IO, Ada.Integer_Text_IO, Day_Of;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        Put ( Day_Of (Day, Month, Year) );
    end Weekday;

4.7 Packages

At the moment the program doesn’t check if the date that the user types in is valid. It’s easy enough to write a function to do this:
    function Valid (Day, Month, Year : Integer) return Boolean is
    begin
        if Year < 1901 or Year > 2099 or Day < 1 then
            return False;
        else
            case Month is
                when 1 | 3 | 5 | 7 | 8 | 10 | 12 =>   -- 31 day months
                    return Day <= 31;
                when 4 | 6 | 9 | 11 =>                -- 30 day months
                    return Day <= 30;
                when 2 =>                             -- February; 28 or 29 days
                    if Year mod 4 = 0 then
                        return Day <= 29;
                    else
                        return Day <= 28;
                    end if;
                when others =>                        -- invalid month
                    return False;
            end case;
        end if;
    end Valid;
The function returns a Boolean result (True or False). It checks that the year is between 1901 and 2099 and that the day is not zero or less; it then uses a case statement to distinguish between the various lengths of month and checks that the day is not greater than the last day of the month. Note that the test for leap years (Year mod 4 = 0) is only adequate when Year is guaranteed to be between 1901 and 2099; it would be better to generalise it in case the range allowed for Year ever changes.

Each branch of the case statement returns a Boolean result, usually the result of comparing Day with the last day of the month. For example, ‘Day <= 31’ is a Boolean expression which will give a result of True if the day is 31 or less and False otherwise; the function just returns the result of the expression directly. The function can be used as the condition part of an if statement (which must evaluate to a Boolean value):

    if Valid (D,M,Y) then
        -- do this if Valid returns True
    else
        -- do this if Valid returns False
    end if;
This function is fairly closely related to the Day_Of function; there are probably dozens of date-related functions we could dream up. It would make sense to gather these functions together into a package rather than having a library full of functions with no apparent relationship to each other.

Another advantage of using a package is to avoid ‘namespace pollution’. Library units must have unique names, so the more procedures and functions we put in our library the more restricted the choice of names becomes. It is far easier to ensure that names are unique if collections of procedures and functions can be entered into the library under a single package name. Also, as you’ll see later, you can minimise the number of unique names even further by using child packages.

The first step is to generate a package specification. This needs to contain declarations for everything we want users of the package to be able to access: variables, subprograms, whatever. In the case of subprograms, we only provide specifications in the package specification; the actual code for subprograms goes into a separate package body. Here’s a first stab at the specification of a package to contain the Day_Of and Valid functions:

    package Dates is
        function Day_Of (Day, Month, Year : Integer) return Integer;
        function Valid  (Day, Month, Year : Integer) return Boolean;
    end Dates;
The package specification goes in a file by itself and must be compiled and added to the library. Next we produce a package body to provide the implementation of the two functions. The package body has the same name as the package specification; the only difference is that it begins with the words ‘package body’ rather than just ‘package’:
    package body Dates is
        function Day_Of (Day, Month, Year : Integer) return Integer is
            D : Integer := Day;
            M : Integer := Month;
            Y : Integer := 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 (((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7);
        end Day_Of;

        function Valid (Day, Month, Year : Integer) return Boolean is
        begin
            if Year < 1901 or Year > 2099 or Day < 1 then
                return False;
            else
                case Month is
                    when 1 | 3 | 5 | 7 | 8 | 10 | 12 =>
                        return Day <= 31;
                    when 4 | 6 | 9 | 11 =>
                        return Day <= 30;
                    when 2 =>
                        if Year mod 4 = 0 then
                            return Day <= 29;
                        else
                            return Day <= 28;
                        end if;
                    when others =>
                        return False;
                end case;
            end if;
        end Valid;
    end Dates;
The package body can now be compiled and added to the library, and then we’re finished. Note that we don’t have to provide a with clause for Dates at the start of the package body; a package body is automatically granted access to its own specification.

We can use this package in any of our programs in exactly the same way as any other package:

    with Ada.Text_IO, Ada.Integer_Text_IO, Dates;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);

        if Dates.Valid(Day,Month,Year) then
            Put (Dates.Day_Of (Day, Month, Year));
        else
            Put_Line ("Invalid date!");
        end if;
    end Weekday;
The program has to include the package in the with clause at the beginning; the declarations in the package specification are then accessible so that we can refer to the functions Dates.Day_Of and Dates.Valid. We could of course name Dates in a use clause so that Day_Of and Valid can be used without having to specify the package name as a prefix.

Note that only the package specification is accessed by a with clause; anything defined inside the package body is local to the package and hence invisible to programs which use the package. The package body might contain extra subprograms which are used to help implement the ones declared in the package specification; for example, there might be a function called Month_Length which returns the number of days in a given month:

    package body Dates is

        -- Day_Of is accessible to users via the specification

        function Day_Of (Day, Month, Year : Integer) return Integer is
            D : Integer := Day;
            M : Integer := Month;
            Y : Integer := 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 (((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7);
        end Day_Of;

        -- This function is local to the package body and
        -- is not accessible via the package specification

        function Month_Length (Month : Integer) return Integer is
        begin
            case Month is
                when 2 =>
                    return 28;
                when 4 | 6 | 9 | 11 =>
                    return 30;
                when 1 | 3 | 5 | 7 | 8 | 10 | 12 =>
                    return 31;
                when others =>
                    return 0;
            end case;
        end Month_Length;

        -- Valid is accessible to users via the specification
        -- and is implemented using the local function Month_Length

        function Valid (Day, Month, Year : Integer) return Boolean is
        begin
            if Year < 1901 or Year > 2099 or Day < 1 then
                return False;
            elsif Month = 2 and Year mod 4 = 0 then
                return Day <= 29;
            else
                return Day <= Month_Length (Month);
            end if;
        end Valid;
    end Dates;
Since Month_Length is not declared in the package specification, it cannot be accessed by programs which use the package; the only place it can be accessed is within the package body itself.

Note that any variables you might declare in the package specification will effectively be global variables rather than local variables; they will be created before executing the main program when the with clause for the package is elaborated, they will not be destroyed until after exiting from the main program, and they will be accessible wherever the package is accessible. Variables declared in the package body itself rather than inside a particular subprogram in the package body will also be created before the start of the program and destroyed after it finishes, although in this case they will not be accessible from outside the package body.

The package body shown above consists entirely of subprogram declarations. You are also allowed to put a series of statements in a package body in exactly the same way as you do in a subprogram (after any declarations, between begin and end):

    with Ada.Text_IO;
    package body Dates is
        ...            -- as above
    begin
        Put_Line ("This program uses Dates by John English");
    end Dates;
The statements after begin are executed when the package body is elaborated, typically at the point where the package is accessed by a with clause just before the main program is executed. They can be used to perform any initialisation that may be needed before the package is used for the first time. This is not a terribly common requirement, but one use for it might be to display a copyright notice of some sort at the start of the main program. In the example above, the message ‘This program uses Dates by John English’ will be displayed whenever any main program which uses the package starts up.


4.8 Child packages

To minimise namespace pollution even further, Ada allows us to define extensions to existing packages which are referred to as child packages. The parent package must have a unique name, but the children of this package have names which use the parent package’s name as a prefix. Since the parent package’s name is unique, this means that the child packages will also have unique names. The number of unique names needed is thus reduced to a handful of parent packages. This can also minimise the risk of bought-in package libraries from different suppliers having names that clash; each supplier can just provide a parent package with a unique name (their company name, for example). You’ve already come across child packages: Ada.Text_IO and Ada.Integer_Text_IO are typical examples. In fact, all the standard packages in Ada are children of one of three parent packages: Ada, Interfaces or System.

I’m going to do the same thing in this book. I’m going to define a parent package called JE (my initials) and make every other package in the book a child of that. Here it is:

    package JE is
        -- an empty package!
    end JE;
This package specification is only there to act as a parent for other packages. Since there’s nothing in it, there’s no need for a package body. In fact, you aren’t allowed to provide a package body if it isn’t needed (i.e. if there are no subprograms declared in the specification). All we need to do is compile the package specification and add it to the library. Now we can rewrite the previous package like this:
    package JE.Dates is
        function Day_Of (Day, Month, Year : Integer) return Integer;
        function Valid  (Day, Month, Year : Integer) return Boolean;
    end JE.Dates;
All I’ve done is to change the name of the package from Dates to JE.Dates. JE.Dates is effectively an extended version of the parent package JE; everything declared in JE is also part of JE.Dates (which doesn’t give you anything extra in this case since JE is an empty package!). In other words, naming JE.Dates in a with clause automatically gives you access to the contents of the parent package JE.

Now the package body’s name needs changing to match:

    package body JE.Dates is
        function Day_Of (Day, Month, Year : Integer) return Integer is
        begin
            -- as before
        end Day_Of;
        function Valid (Day, Month, Year : Integer) return Boolean is
        begin
            -- as before
        end Valid;
    end JE.Dates;
and the main program needs changing to refer to JE.Dates instead of Dates:
    with Ada.Text_IO, Ada.Integer_Text_IO, JE.Dates;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        if JE.Dates.Valid(Day,Month,Year) then
            Put (JE.Dates.Day_Of (Day, Month, Year));
        else
            Put_Line ("Invalid date!");
        end if;
    end Weekday;
The main effect of this change is that only the name JE needs to be unique in the library; something else called Dates will not clash with JE.Dates.

It is also possible to declare subprograms as children of packages; the function Day_Of could have been declared as a child of the package JE like this:

    function JE.Day_Of (Day, Month, Year : Integer) return Integer is
        D : Integer := Day;
        M : Integer := Month;
        Y : Integer := 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 ((26*M - 2)/10 + D + Y + Y/4 + C/4 - 2*C) mod 7;
    end JE.Day_Of;
All that’s changed from the original version of the function is the name (JE.Day_Of instead of Day_Of). The main program will have to have a with clause referring to JE.Day_Of to use it:
    with Ada.Text_IO, Ada.Integer_Text_IO, JE.Day_Of;
    use  Ada.Text_IO, Ada.Integer_Text_IO;
    procedure Weekday is
        Day, Month, Year, Century : Integer;
    begin
        Put ("Enter a date: ");
        Get (Day);
        Get (Month);
        Get (Year);
        Put (JE.Day_Of (Day, Month, Year));
    end Weekday;

Exercises

4.1 The procedure Ada.Integer_Text_IO.Get skips any leading spaces and then reads an integer, but Ada.Text_IO.Get reads a character without skipping leading spaces. Write a procedure called Get_Non_Space which skips leading spaces and reads the next non-space character into the variable supplied as its parameter and write a simple program which tests that it works properly.

4.2 Write a procedure which takes three integer parameters representing a date and a fourth integer parameter representing a number of days and adds the specified number of days to the date (so that, for example, adding 7 to December 26th 1995 will give January 2nd 1996). Add this procedure to the Dates package described in this chapter and write a simple program which tests that it works properly.

4.3 Write a function to calculate the greatest common divisor (GCD) of two integers. You can do this using ‘Euclid’s algorithm’: the GCD of two values X and Y is X if Y is zero, otherwise the process should be repeated using the values Y and X mod Y instead of X and Y respectively (e.g. for X = 9, Y = 6, since Y isn’t zero the process is repeated with X = 6, Y = 9 mod 6 = 3; since Y still isn’t zero it’s repeated again with X = 3, Y = 6 mod 3 = 0, and this time Y is zero so the result is 3). The absolute value (positive magnitude) of the result should be returned (you can get the absolute value of a number X using the expression ‘abs X’). Write a simple program which tests that it works properly.

4.4 Many computers have an ‘ANSI-compatible’ display. Write a package to do simple screen handling for an ANSI-compatible display which contains procedures called Clear_Screen and Move_Cursor:
    procedure Clear_Screen;
    procedure Move_Cursor (Row, Column : in Integer);
You can perform these operations by using Put to display ‘escape sequences’ consisting of the ‘escape’ character (which can be referred to using the name ASCII.ESC) followed by a string representing a screen management command, e.g. "[2J" to clear the screen or "[5;6H" to move the cursor to row 5, column 6 on the screen.



Previous

Contents

Next

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