This text has employed procedures many times already, even though no formal definition of the concept has been supplied. Any time a program uses something like:
WriteString ("answer here"); ReadReal (theNumber);
or
INC (aNumber);
it relies on code found elsewhere to operate upon some item(s) and/or to obtain a result. Many of the built-in and imported identifiers name procedures.
A procedure is a unit of code designed to perform a particular sub-task of some main task. It is elaborated (written out) only once in some module, but can be used many times.
One way to think of a procedure is as a black box that performs some action whenever it is invoked by the flow of the program. The action taken may:
(i) be the same every time, as in:
WriteLn;
(ii) depend on data supplied to it, as in:
WriteString (string to write);
(iii) produce or alter data:
ReadCard (theNumber);
(iv) involve both (ii) and (iii):
ch := CAP (ch); INC (theNumber, theIncrement);
The possibilities are summarized in figure 4.1.
A procedure may best be regarded as code that is subordinate to a program in its operation--in the sense that the main program suspends operation in order to call the procedure to execute its code. When the latter is finished, the main program resumes control.
Besides the built-in procedures named by standard identifiers, (CAP, INC, etc.) and the ones available from various libraries, (WriteString, ReadCard, etc.) a program may define and use its own procedures.
Write a program that can compute the voltage in a circuit given the voltage, current and resistance.
The relationship among these three is based on Ohm's law, which can be expressed as E = IR where E is the voltage, I the current in Amps, and R the resistance in Ohms. The initial formulation of the solution is scarcely changed from programs in previous chapters, except that the routines that print out the program header have been removed to a procedure of the first type above--one that neither produces nor consumes data, but merely takes an action.
MODULE OhmsLaw1; (* Written by R.J. Sutcliffe *) (* to illustrate a simple procedure *) (* using P1 Modula-2 for the Macintosh computer *) (* last revision 1993 02 25 *) FROM STextIO IMPORT WriteString, WriteLn, ReadChar, SkipLine; FROM SRealIO IMPORT ReadReal, WriteFixed; (**********************************************) PROCEDURE DisplayInfo; BEGIN WriteString ("OhmsLaw1 was written by R.J. Sutcliffe"); WriteLn; WriteString ("to illustrate a simple procedure"); WriteLn; WriteLn; WriteString ("This program computes the voltage in a circuit"); WriteLn; WriteString (" given the current and resistance."); WriteLn; END DisplayInfo; (**********************************************) VAR voltage, current, resistance : REAL; answer : CHAR; BEGIN DisplayInfo; (* invoke the above procedure *) (* Gather the information from the user *) WriteString ("What is the current in amperes? ==> "); ReadReal (current); SkipLine; WriteLn; WriteString ("What is the resistance in ohms? ==> "); ReadReal (resistance); SkipLine; WriteLn; (* Now, compute the voltage. *) voltage := current * resistance; WriteString ("A current of "); WriteFixed (current, 2, 0); WriteString (" amps running through a resistance of "); WriteFixed (resistance, 2, 0); WriteLn; WriteString (" ohms produces a voltage of "); WriteFixed (voltage, 2, 0); WriteString (" volts. "); WriteLn; WriteLn; WriteString ( "Press any key to continue."); ReadChar (answer); END OhmsLaw1.
Here is a sample run:
OhmsLaw1 was written by R.J. Sutcliffe to illustrate a simple procedure This program computes the voltage in a circuit given the current and resistance. What is the current in amperes? ==> 12.3 What is the resistance in ohms? ==> 14.2 A current of 12.30 amps running through a resistance of 14.20 ohms produces a voltage of 174.66 volts. Press any key to continue.
NOTES: 1. PROCEDURE is a reserved word. It marks the commencement of a subordinate section of code in much the same way as the word MODULE marks the start of the main program.
2. User defined procedures are named with the usual identifier rules. It is customary to begin a procedure name with an upper case letter, and to use for its name a verb that describes its action.
3. A procedure is defined by placing the heading and block of code in the declaration section of a program module, along with the variable declarations.
4. A procedure defined in a program is run (or invoked) by using its name, just as for the built-in ones.
5. Since it has a named "block" of code (like a Module), the procedure must also have that same name with its END.
Write a module that converts percentages into letter grades and prints them out.
Most of the work in the program below has been pushed off into the procedure that accepts as data the average mark and selects and prints the letter grade. To shorten things down somewhat, this program writes no header at all.
MODULE LetterGrade; (* Written by R.J. Sutcliffe *) (* to illustrate a data consuming procedure *) (* using P1 Modula-2 for the Macintosh computer *) (* last revision 1993 02 25 *) FROM STextIO IMPORT WriteString, WriteLn, WriteChar, SkipLine; FROM SRealIO IMPORT ReadReal, WriteFixed; FROM SIOResult IMPORT ReadResult, ReadResults; VAR theMark : REAL; ok : BOOLEAN; (************************************************) PROCEDURE PrintGrade (average : REAL); VAR grade : CHAR; BEGIN IF average < 40.0 THEN grade := "F" ELSIF average < 50.0 THEN grade := "D" ELSIF average < 73.0 THEN grade := "C" ELSIF average < 86.0 THEN grade := "B" ELSE grade := "A" END; WriteString ("an average mark of "); WriteFixed (average, 2, 0); WriteString (" percent earns a letter grade of "); WriteChar (grade); WriteLn; WriteLn; END PrintGrade; (************************************************) BEGIN WriteString ("This program converts averages into grades. "); WriteLn; WriteString ("Enter an invalid real when done. "); WriteLn; REPEAT WriteString ("enter the average mark to convert here ==> "); ReadReal (theMark); ok := (ReadResult() = allRight); SkipLine; WriteLn; IF ok THEN PrintGrade (theMark); END; UNTIL NOT ok; END LetterGrade.
Here is an output run:
This program converts averages into grades. Enter an invalid real when done. enter the average mark to convert here ==> 12.0 an average mark of 12.00 percent earns a letter grade of F enter the average mark to convert here ==> 34.0 an average mark of 34.00 percent earns a letter grade of F enter the average mark to convert here ==> 56.0 an average mark of 56.00 percent earns a letter grade of C enter the average mark to convert here ==> 78.0 an average mark of 78.00 percent earns a letter grade of B enter the average mark to convert here ==> 89.0 an average mark of 89.00 percent earns a letter grade of A enter the average mark to convert here ==> aaa
NOTES: 1. In the example above, average is a temporary variable to which the value of theMark is assigned. This allows the value of theMark to be manipulated inside the procedure.
2. No variables declared inside a procedure (like grade) have any meaning outside it. If the procedure is re-entered later, its variables must be re-initialized if they are to have a specific value. This also means that variables in one procedure may have the same names as those in another one, or as in the main program. The compiler will not become confused by this, but the programmer may.
3. A consequence of this last point is that, when a reference to some identifier is made inside a procedure, a check is first made to see if that name is defined in the procedure. If so, that is the one used; if not, the enclosing procedure is checked next, and so on out to the main module. See a later section on scope for a further discussion of this.
Write a procedure that can be used in various programs to obtain data from the keyboard with some error checking.
Procedures of this kind are often used to avoid writing out the same (or a very similar) statement sequence several times in a single program. Instead, the code is made generic and then is called upon from all the appropriate places in the program. Examples in this chapter will use variations on the following:
PROCEDURE GetNum (VAR theNum : REAL); VAR ok : BOOLEAN; BEGIN REPEAT WriteString ("Type the number here ==> "); ReadReal (theNum); ok := (ReadResult() = allRight); (* save result here *) SkipLine; (* because this will change ReadResult() *) IF NOT ok (* use saved value for testing *) THEN WriteLn; WriteString ("error in input number; try again."); WriteLn; END; UNTIL ok; END GetNum;
When the main program invokes this procedure, it may print part of the prompt itself, then call on GetNum with a variable name of its own. Data assigned to a local variable whose name in the procedure heading is preceded by VAR will be passed back to the program variable used when the procedure is invoked. That is, if this procedure is invoked by the line
GetNum (sideLength);
then the program variable sideLength will be assigned whatever value is read by the procedure into the variable it calls theNum.
Write a procedure to round off real numbers to the nearest integer.
This problem is not a new one; the code to do this came up during the course of section 3.12, program DateCalc, where it was observed that there is no such round off procedure built in to Modula-2 (though there is one in the library for integers.) Here is one way to write the code:
PROCEDURE Round (input : REAL; VAR output : CARDINAL); BEGIN output := TRUNC (input + 0.5); END Round;
This procedure would be invoked in a program by writing
Round (numToRound, roundedNum);
where numToRound is a real variable, and roundedNum is a cardinal variable. Notice, however, that this procedure does not take into account the fact that the real input may be negative. One of the exercises asks the reader to make the necessary modifications.
This introduction to procedures is concluded by giving some additional formal definitions.
The list of variables in parentheses after the name of a procedure is called a parameter list.
An invocation of a procedure is the occurrence of its name somewhere in a program. The invocation instructs the processor to execute the code of the procedure.