ADVANCED SUBPROGRAM TOPICS
In part 1 of this tutorial we covered the topic of subprograms in some detail, but there are many other things to discuss about them, so we return to them for more advanced topics.
Example program ------> e_c18_p1.ada
Examine the program named e_c18_p1.ada for some examples of default parameters used in the definition of a procedure. The procedure has four formal parameters, of which the first is of mode in out, and the other three are of the mode in. The three in parameters have default values of zero assigned to each of them. When we call this procedure, we are not required to supply a value for each variable, and those we do not supply a value for will be defaulted to zero upon execution. Of course the first variable in the list, named Total, must have a variable name supplied so it can return a value. Therefore, it cannot be defaulted.
The procedure itself, and the first two calls to it, in lines 29 and 30, should pose no problem for you to understand. When we arrive at line 31, however, we have a few things to point out.
NAMED NOTATION FOR ACTUAL PARAMETERS
We are using the named aggregate notation for the actual parameters in line 31, so they can be listed in any order, but due to the defaults defined in the procedure header, we do not have to specify every parameter, allowing the default values to take effect upon a call to the procedure. You will see, when you compile and run this program, that Cows and Pigs will have the default values of zero. Lines 32 and 33 also use the named aggregate notation and should be clear as to their operation. Line 34 uses the mixed aggregate notation, and as we discussed before, the positional aggregate notation can be used initially, but after switching to the named notation, all remaining entries must be named, unless they are allowed to default. Line 35 illustrates the degenerate case where only the result is used, with all three input variables defaulting to zero.
PARAMETERS WITH out MODE CANNOT BE DEFAULTED
Since the parameters that are of either mode in out or mode out must be able to return a value, they must have a variable defined as their actual parameter, and cannot therefore be defaulted.
Default parameters are not new to you, because you have actually used them in procedure calls before. When you call the procedure New_Line, you have an optional number following it as in New_Line(2). The number of lines to space up on the monitor is defaulted to one in the package Ada.Text_IO, which was supplied to you with your compiler, but you can override the default by inserting a value for the number of lines. It is defined in Annex A.10.1 of the Ada 95 Reference Manual (ARM), where the formal parameter named Spacing is defaulted to the value of 1. Refer to either your documentation or the ARM and see this in use.
DYNAMIC DEFAULT PARAMETERS
Example program ------> e_c18_p2.ada
The program named e_c18_p2.ada is identical to the last example program except for one detail. The definition of the default values of the formal parameters are declared differently here. You will notice that the default values are not only arithmetic combinations, but the values to combine are the results of function calls, where the values are dynamically evaluated each time the procedure Animals is called. The default values are constants for each call of the procedure, but they are evaluated for each call and could therefore be different each time the procedure is called and executed. As mentioned previously, this is called elaboration. If the return from Cow_Constant, in line 12, returned the value of a global variable for example, the main program could modify the value of the global variable and therefore modify the value of the default variables prior to each call to the procedure. It would therefore be possible to set up different default values in each of several different procedures based on the currently read time of day, the ambient temperature, or whatever other variable conditions could be read into the system.
Be sure to compile and run this program and observe the results, comparing them with the results you expected the program to output.
THE MYSTERY OF RECURSION
Example program ------> e_c18_p3.ada
This topic will be no problem for the experienced Pascal programmer, but for the FORTRAN programmer, it may be an entirely new and somewhat perplexing topic. Stay with it, and you will see exactly what recursion is and how to use it effectively. Examine the example program named e_c18_p3.ada, which is the simplest recursive program possible, but which is excellent for describing what recursion is and how it works.
Beginning at the main program, we assign the variable named Index the value of 7 then call the procedure named Print_And_Decrement, taking along the value of Index as an actual parameter. Arriving at the procedure itself, we use the name Value for the formal parameter, and we display the value on the monitor with an appropriate line of text. Continuing on to line 15, we decrement the value of the passed variable then compare the result to zero. If the value is greater than zero, we call the procedure named Print_And_Decrement taking along the newly decremented value called New_Value. Here is where the FORTRAN programmer notices something new. We are calling the procedure from within itself, and that is just what recursion is. Assume for a moment that we call another complete copy of the procedure, decrement the value once again, and if it is still not zero, call another copy of the procedure. Eventually, the value of the passed variable will be reduced to zero and the procedure calls will all be completed, each returning to the procedure that called it until we arrive once again at the main program.
You should compile and run this program to see that it really does what we say it does, then return for additional discussion of this program and what it is doing. This is a really dumb way to count from 7 down to 1, but it is a very simple way to illustrate the use of recursion. Later in this tutorial, we will have illustrations of excellent uses of recursion for your instruction.
WHAT ACTUALLY HAPPENED?
When we called the procedure Print_And_Decrement, it began by elaborating its formal variables and assigning them the values passed by the calling program. These are stored on the stack, an internal portion of memory set aside by the Ada system to store dynamic variables and constants, and are available for later use. The local variables are then generated and elaborated, although in this case there was only one, and stored on the stack also with means to refer to them. Finally, the program code itself is actually executed, and when it is completed, the local variables and formal variables are erased from the stack and no longer exist. In a recursive call, the stack grows with each new call, and when control returns to an earlier call of the code, the variables for that call are still on the stack and available for use. In this program, we actually have only one copy of the executable code stored, but we have many copies of the formal variable and the local variable stored on the stack.
ALL ADA SUBPROGRAMS ARE RE-ENTRANT
If you have experience in systems programming and understand what it means for a program to be re-entrant, you will understand how this means of variable storage allows all Ada subprograms to be re- entrant. The ARM requires that all Ada subprograms be re-entrant, but if you don't know what it means, don't worry about it.
It would be a good exercise for you to insert some code to display the value of the formal parameter following the recursive call to see that the value of the formal variable is still available when we return from each recursive call. This code would be inserted immediately after line 18.
You should spend the time necessary to completely understand this program before continuing on to the next example program.
A RECURSIVE FUNCTION
Example program ------> e_c18_p4.ada
Examine the program named e_c18_p4.ada for an example of a recursive function, which is actually no different than a recursive procedure. This program is used to illustrate two other Ada concepts, neither of which is new to you, but both of which contain valuable insights for you. The first is the partial declaration which will be discussed at the outset, and the other is a return to exceptions which will be deferred for a couple of paragraphs.
THE PARTIAL DECLARATION
Ada requires that you define everything prior to its use, and this rule is never broken. Suppose you wished to have a program with two procedures, two functions, or even a procedure and a function, calling each other recursively. If A were declared first, it could be called by B, but A could not call B because it would not be defined by the time A was elaborated, and we cannot break the rule of defining everything prior to its use. This is more properly called linear declaration. Ada gets around this by allowing a partial declaration of a type, procedure, function, or package. If you remember, we used the partial declaration with respect to a record when we studied the access type variable, so this concept is not entirely new to you. It is also proper to refer to the partial declaration as a function specification.
In the present example program, we desire to place the functions in the program in the order shown, for no good reason other than to illustrate that it can be done, but we have the problem of linear declaration because Factorial calls Factorial_Possible and they are in the wrong order. The partial declaration in line 12 tells the system that the function Factorial_Possible will be defined later and what its characteristics will be, because the formal parameter is defined along with its return type. The function Factorial is then completely defined, and it can call the other function because it has the definition of its interface. We then have the complete definition of the function Factorial_Possible and we have accomplished our desired goals, while meeting the requirements of linear declaration.
NOW FOR THE RECURSIVE FUNCTION
Assuming you are familiar with the factorial function which is a part of higher mathematics, we will continue with the program description. Since Factorial(N) is the same as N times Factorial(N-1), we can calculate the factorial of any number using a recursive technique. We continue to recurse until we reach a point where we need the value of Factorial(1), which we define as 1, and begin going back up the recursive chain, each time multiplying by the value with which we entered into that particular recursive call. By defining the value of Factorial(0) as 1, and making it illegal to take the factorial of any negative number, we can now write the complete program. Most of the program involves checking limits and outputting messages, but the actual work is done by line 27 which is the recursive call as defined earlier in this paragraph.
EXTRA CHECKS ARE DONE
The program should not be at all difficult for you to follow and understand, so you will be left on your own to study it. A few comments on style should be mentioned, however. The function Factorial calls the other function to verify that the value is factoriable before attempting to do so, and the main program, in lines 44 through 54, checks the value before calling the function to attempt to factorialize the number. A redundant check like this is not necessarily bad, because the procedure was written to be all inclusive, and the main program may wish to do something entirely different than that dictated by the procedure. In lines 58 through 63 however, the main program accepts the default error handling provided by the procedure, by calling the procedure without first checking the data.
Compile and run this program, observing the various messages output depending on which portion of the program handled the errors. Note carefully that the program is not meant to be an illustration of good programming style, only as an illustration of a few things that can be done using Ada.
ANOTHER LOOK AT EXCEPTIONS
The last program was unusual because it has the ability to illustrate three of the four standard exceptions defined by the Ada system. They can be illustrated as follows;
Example program ------> e_c18_p5.ada
The example program named e_c18_p5.ada will give you an example of a function that returns more than a single scalar variable, in fact, it returns an entire array of INTEGER type variables. The program itself is extremely simple, the only thing that is different from most functions is the type of return listed in line 12, and the actual return in line 18. These must, of course, agree in type or a type mismatch error will be issued during compilation.
Using the technique given here, there is no reason why a function cannot return a record, or even an array of records, as long as the types are correctly defined and they agree when used.
THE INLINE pragma
A pragma is a compiler directive, as we have mentioned previously, and the INLINE pragma tells the compiler to expand the called subprogram body and insert it into the calling program for each call. Since the code is inserted inline, there is no calling sequence and therefore no time wasted in setting up subprogram linkage, but there is a separate section of code for each invocation of the subprogram. The result will usually be a larger but faster program. Use of this pragma is illustrated as follows;
pragma INLINE(subprogram1, subprogram2, ... );This line is inserted in the declarative part of the compilation unit, following the subprogram specifications. By definition, the meaning of a subprogram is not affected by the pragma.
OPERATOR OVERLOADING AND THE "use" CLAUSE
Example program ------> e_c18_p6.ada
Operator overloading has been mentioned in this tutorial before, but it is necessary to cover a little more ground on this very important topic, and we will use e_c18_p6.ada as a vehicle for discussion.
We define a package specification in lines 2 through 18 named Shape with a record type named BOX defined within the package. The subprograms Make_A_Box and Print_Box provide us with the ability to generate and display any variable of the BOX type, and we provide three overloaded operators for use with this type. Two of the subprograms overload the + operator for use with variables of this type, and the other provides us with a multiply capability. Many more overloadings could be defined if desired, but these three illustrate the technique.
The body for the package is given in lines 22 through 75. The overloaded operations make little sense in the real world, since adding two boxes would not usually be done by simply adding the size of all three dimensions. However, we are far more interested in the technique of overloading operators, so the interface is what really matters here. The implementation should be simple for you to study on your own.
THE CALLING PROGRAM
The calling program is definitely the most interesting part of this example, beginning with line 80 where we have a use type construct which is new to us. A short digression is in order at this point to explain what this does.
Many experienced Ada programmers feel that the use statement should never be used in an Ada program because it hides the source of any subprogram calls or type definitions. For this reason many projects outlaw its use. It seems reasonable to this author, that Ada.Text_IO should be a permitted violation of this rule, but that is yet another digression so we will not consider it further. In order for the overloaded operators to be available for use as infix operators, as illustrated in lines 94 through 97 of this example program, the use clause must be included. If it is not included, line 97 would need to be written as illustrated in line 99, a very ugly and difficult to read notation. The use of the use type construct in line 80 makes the overloaded operators available for use as infix operators, but does not permit the use of any other subprograms from the Shape package without the package prefix. This gives the ease of readability, and the extra safety desired by some project leaders.
The package name is required when defining variables in lines 84 and 85, and when calling subprograms that are not operator overloads, such as illustrated in lines 89 through 99. Note that line 97 calculates the result we expect because the multiplication is done prior to the addition. It is not possible to change the order of precedence when overloading operators.
Return to the Table of Contents