Procedures Peter Suber, Computer Science, Earlham College A procedure is a mini-program inside a program. It can do virtually everything that a program can do. Usually, however, we want procedures to do relatively small jobs. Then we execute them one after another to get bigger jobs done. To think clearly about complex tasks, we break them down into simpler tasks. Procedures allow us treat arbitrary clusters of simple tasks as unified chunks with single names. The complexity suddenly has manageable parts. Procedures help us master complexity.
Procedures have names. To execute a procedure we simply type its name as a Pascal statement. Since the name of a procedure works like an action statement, it is followed by a semi-colon. For example, if we had written a procedure called "SoundEffects" we might use it in these ways:
(1)if (Score > 93) then SoundEffects;
(2)write('What''s your name?');
readln(Answer);
SoundEffects;The statement calling the procedure by name in order to execute it is called a procedure call.
A procedure call does in one statement what you would otherwise have to do by entering every statement inside the procedure. Obviously the procedure call is an enormous labor-saving device. If we want the procedure's action to occur more than once, the labor saved is greatly increased.
The fact that a complex or long list of statements need only be typed once has other important consequences for programming craft. Typing errors are minimized. Humans find the code much easier to read and understand. Intelligent revision is much easier. Without procedures, we'd have to revise the many separate action statements wherever they occurred. But if they are packaged in a procedure, we only have to type them once and revise them in one place.
Moreover, procedures allow us to turn many simple actions into one action for frequent reuse. Once our procedures are fine-tuned, they can be pulled out like standard parts from a shelf. (A procedure can be reused as a standard part as often as you like in the program in which it is declared; if you put it in your library.pas file, it can be used in any and all your programs as a standard part.) Once they are 'standard parts', complex actions are as easily available to us as simple actions were when we started programming. This is one virtue of modularity in programming. Languages that permit it, like Pascal, may as well be infinite: they can be extended in any direction as far as you like.
Hiding a lot of complexity in one place behind a single name not only tames complexity, saves labor, prevents mistakes, helps readers, aids revision, and promotes modularity. It helps us to think about what we are doing at a higher level of abstraction. If the sound effects take 15 separate action statements, then by chunking them into one procedure call we have abstracted from all that distracting detail. The many actions chunked into one procedure can participate in further chunking. Procedures can consist of many other procedure calls, chunking complexity into simplicity layer by layer.
When we have many actions to perform, we can break them into logical chunks with informative names, forget their detail, and consider only a few procedure calls.
(3)if (Score > TenthHighestScore) then begin
SoundEffects;
Congrats;{writes congratulatory messages to screen}
HallOFame;{saves player's name to file of high scorers}
Purge;{removes 11th highest scorer from hall of fame}
SoundEffects;{repeated for truly serious exultation}
Reset;{re-initializes some variables, start new game}
end; {then}This if statement is very easy to read, write, understand, and revise. The logical flow of operations is clear. Imagine what this code would look like if each of these procedures had 5-50 lines of code, and we did not use procedure calls.
Declaring procedures
The syntax of declaring procedures reminds us that procedures are mini-programs inside larger programs. Declaring a procedure is almost exactly like declaring a Pascal program. Here's the skeleton of a SoundEffects procedure:
(4)procedure SoundEffects;
const duration = 30;
var pitch : integer;
begin
{blah blah...}
{These statements do the work of making sound effects.}
end; {procedure SoundEffects}Note how similar it is to the structure of a general Pascal program.
- Similar: Instead of the reserved word program, we start with the reserved word procedure.
- Same: We follow with an identifier naming the procedure, just as we name a program. This is the name used in procedure calls.
- Same: Then we define constants and declare variables (if we like).
- Same: Then in between the reserved words begin and end, we insert the action statements comprising the procedure (the "statement part").
- Different: A program ends wth a period; a procedure ends with a semi-colon. But this is logical!
A procedure is not a microcosm of a whole program just for elegance. It can be a whole program. Anything a program can do, a procedure can do, and vice versa. (This is not quite true; but the exceptions are trivial, so don't worry about them.) Once we have written a program to do something, we can make it a mere procedure call in a larger program that does something more difficult and complicated. Imagine what can be accomplished by compounding complexity in this way.
Procedures are like variables: they must be declared before they are called. They are declared in the declaration part of a program, with the variable declarations. It's that simple.
The declaration part of a program and the declaration part of a procedure are strictly analogous. Anything that can go in one, can go in the other. This means that a procedure can contain other procedures. This shows the macrocosm/microcosm relation of programs and procedures dramatically.
Parameters
I've written a procedure that draws a box on the screen. To use it, copy the file "drawbox.pas" from public network drive (o:\public\suber) to your own disk. Then make {$I B:drawbox} the second line of your program instead of {$I B:library}.
The details of this instruction will change from year to year, and will not be useful to those reading this from off-campus. I've kept references like this to a minimum in this hand-out.We'd like to be able to tell the procedure where on the screen to place the box and what size to draw it. This requires four pieces of information:
- The column of the left edge of the box (how far to the right of the left edge of the screen),
- the row of the top edge of the box (how far down from the top of the screen),
- the width of the box, and
- the height of the box.
If you like, think of the screen as a grid in which each character has x and y coordinates. Items 1 and 2 above give us the x and y coordinates of the upper left corner of the box. Items 3 and 4 give us the width and the height.
(5)DrawBox(5,6,7,8);
would draw a box whose upper left corner was in column 5, row 6; the box would be 7 characters wide and 8 characters high.
Write a program to try it; for example:
(6)program BoxPlay;
{$I B:drawbox}
begin
DrawBox(5,6,7,8);
readln;{so program doesn't end til we hit RETURN}
end.(Remember that our screens are 80 characters wide and 25 characters high. If you ask for a box wider than 80 or taller than 25, you'll get funny results. The upper left corner of the screen is [1,1], not [0,0].)
The numbers we put in parentheses after the procedure's name are the arguments of the procedure call. We've seen arguments to procedure calls before. For example, "writeln" is a Pascal procedure. The text to be written is an argument to the procedure call:
(7)writeln(Age,HatSize,Gender,BowlingHandicap); {4 arguments}
(8)writeln('This quoted string is a single argument.');
Separate arguments by commas when there is more than one.
Writeln is a standard procedure that we don't have to declare ourselves. When we write our own procedures, we have to give them the capacity to take arguments in their procedure calls. We do so in the procedure header when we declare the procedure. Here is the skeleton of DrawBox:
(9)procedure DrawBox(UpLeftX,UpLeftY,Width,Height : integer);
begin
{blah blah...}
{These statements draw the box, using the values}
{passed to DrawBox in the integer variables}
{UpLeftX, UpLeftY, Width, and Height.)
end; {procedure DrawBox}Note the parenthetical statement in the procedure heading (the top line). Four variables are declared to be of type integer. This is called the procedure's parameter list (for reasons to be explained in a minute). When we write a procedure call, for example, in (5) above, then our first argument is passed to the first variable in the list, our second argument is passed to the second variable in the list, and so on.
So to write this simple procedure call
(10)DrawBox(5,6,7,8);
is like writing
(11)UpperLeftX := 5;
UpperLeftY := 6;
Width := 7;
Height := 8;
and then writing all the action statements inside the procedure.
Programmers often use the same word ("parameters") for the arguments in the procedure call and the variables in the procedure declaration heading. But teachers and students of programming must be more precise. (Similarly, speakers of English don't have to know the technical terms for the objective and subjective cases, provided they use she and her correctly, but teachers and students of English must have these terms.)
So we must wade a bit into technicalities. The good news is that when you are fluent, you can forget these technicalities.
The parenthesized expressions in the procedure call are called arguments; the variables declared in the procedure's heading are called parameters. When we must be precise, we'll use these terms in these senses. When we needn't be precise, we can call them both arguments, or both parameters.
The word "parameter" is a stumbling block for many students. It is aptly chosen, but a more familiar word like "specification" or a phrase like "target variable" or "aspect to vary" might have helped new programmers understand better what is going on. A parameter of something is an aspect of it that may vary. So some parameters of a human being are: age, hat size, gender, bowling handicap, nation of citizenship, average speed of hair growth in kilometers per second, number of pairs of shoes, chronic diseases, specific gravity, calls for jury duty, lifetime GPA, and so on. Parameters need not be definite or quantitative; other human parameters are fondness for racoons, compulsion to see every prison-break movie ever made, politeness when sweating, skill at the bugle.
We need the concept of parameters for science. If we give an experimental drug to a group of women, and a placebo to a control group of men, then the two groups would differ in an important parameter that might explain any difference in their response to the drug. If we want to isolate the effect of the drug, the two groups should share all the parameters we think are relevant to the drug's action.
The programming concept we are talking about is that of a parameter of a procedure. We could write different box-drawing procedures for every size and location our program would need. But it is much more economical and elegant to write a single, general procedure whose aspects or parameters can be varied at will at the time of the procedure call.
This is a most powerful capability, and one extremely helpful for good programming. So if the word "parameter" is new and strange, remember that parameter passing is not as arcane as the word for it. It is a natural extension of the usefulness of procedures. If we couldn't pass a procedure information at the time we called it, then it would always do exactly the same thing. But if we want the box to vary in its size or location (two of its important parameters), we want to give it new orders each time we call it. The basic box-drawing operations remain hidden from view, but they use the values we pass to them.
Now that we have the distinction between arguments and parameters, we can describe more clearly what happens when we issue a procedure call with arguments. Let's stretch out this procedure declaration and procedure call,
Procedure DrawBox(UpLeftX,UpLeftY,Width,Height : integer);
DrawBox(5,6,7,8);so that the former's parameters can be aligned with the latter's arguments, as in this table:
Procedure DrawBox( UpLeftX, UpLeftY, Width, Height, : integer); := := := := DrawBox( 5, 6, 7, 8 ); The procedure is declared before it is called, so I have listed the procedure declaration first. When called, the first variable in the parameter list is initialized to the first argument in the call; the second variable in the parameter list is initialized to the second argument in the call, and so on.
When we call a procedure more than once with different arguments in the call, we are setting the parameters to different values to get a different result (in this case a box of a different size and/or in a different location). The parameters of the procedures --the aspects of the procedure that may vary-- are specified in the arguments of the procedure call. Procedures with parameters are general, and will not always do the same job. We give it new orders every time we call it with new specifications.
Imagine how to write the procedures called in this skeletal program.
(12)program Grades;
{constant definitions, variable declarations, and}
{procedure declarations go here}
begin
GetGradeFromUser(Course,Assignment,Student);
ComputeWeightedAverage(Student);
ShowWhoNeedsHelp(Course);
ComputeCurve(Assignment);
ShowAssignmentSummary(Assignment);
ShowStudentSummary(Student);
SoundEffects;
Save(NewData,CourseFile);
end.Imagine writing this program without procedures.
Scope rules
Before we continue with parameters, we must digress on the boundaries of mini-programs nested inside programs.
Let us say that variables declared inside procedures are local variables, and those declared in the program's declaration part are global variables.
Suppose that a program declares a global variable of type integer called Amount. And suppose a procedure inside that program declares a local integer variable also called Amount. Is that legal? Will the program compile? The answer is yes.
But when we are inside the procedure using Amount, does it have the local or the global value? The answer is: the local value.
Local and global variables may have the same name and type without conflicting. Local variables always supersede global variables. This policy is called local precedence. It means that when the procedure is running, the local value of Amount is used; when the procedure is finished, the global value is used again.
Challenge. Why local precedence? Why not global precedence? Can you think of a good reason for favoring local precedence? Would you favor local precedence if you were designing (or revising) Pascal?
We will say that the portion of a program that has access to the value of a variable is the scope or block of that variable. The program itself is the largest block; every procedure within the program is a smaller block nested within that outermost block. For example, if we write a program called Outer, which contains a procedure called Middle, which in turn contains a procedure called Inner, then the block structure of that program would look like this:
Program Outer;
var Amount : integer;begin {mainline}
Procedure Middle;
var Amount : integer;begin {procedure Middle}
Procedure Inner;
var Amount : integer;
begin {procedure Inner}
Amount := 3;
writeln(Amount); {writes "3" to screen}
end; {procedure Inner}
Amount := 2;
Inner; {writes "3" to screen}
writeln(Amount); {writes "2" to screen}
end; {procedure Middle}
Amount := 1;
Middle; {writes "3", then "2" to screen}
writeln(Amount); {writes "1" to screen}
end. {mainline}Variables declared inside a box (block) are accessible only within those boxes.
Note how these nested blocks illustrate local precedence. The integer variable "Amount" is declared and given a value in each block. The variables are distinct and do not interfere with one another. When a writeln() statement writes its value, it writes out the value it possesses in the same block as the writeln() call.
What is important for the programmer is to know that block structure means that the scope of a variable comes to an end. If it is a local variable, it is inaccessible outside its own block. If it is a global variable, it is inaccessible inside a sub-block that has a local variable of the same name and type.
We've been talking about the scope rules as if they applied only to variables; but in fact they apply to everything in a Pascal program that can be declared. So in addition to variables we must include constants --and procedures. If we have procedures inside procedures inside procedures, then it is possible to have two procedures of the same name but different scopes. Procedure calls to such procedures follow the scope rules. The more local always take priority over the less local.
Challenge. If we love the way procedures tame complexity, save labor, prevent mistakes, help readers, aid revision, and promote modularity, abstraction, and chunking, then we might feel frustrated by these scope rules. We will want to use procedures for every job, in all circumstances, in the most flexible possible way. While we might see that some scope rules are necessary, we note that procedures must be declared before they are used, and that the present scope rules are strongly hierarchical. Together, these policies prevent us from (1) having a procedure call itself, and (2) having two procedures call each other. Niklaus Wirth chose to make both these practices possible by small amendments to the rules we've seen so far. If you were Niklaus Wirth, how would you do it? (We'll see Wirth's solutions in due time.)
Two kinds of parameter
Imagine that you are a programmer for Ellis Island in the days when it was the main point of entry for immigrants to the United States. Your bosses (named Fettucini and Guacamole) want a procedure for converting foreign-sounding names to American-sounding names.
(13)procedure Americanize(var FirstName, Surname : string);
begin
{analyze FirstName and Surname; convert to good}
{American-sounding names.}
end;We might use Americanize in a program like this:
(14)write('What is your first name?');
readln(First);
write('What is your surname?');
readln(Second);
Americanize(First, Second);
Writeln('Your new name is ',First,' ',Second,'.');
The user would see this:
(15)What is your first name? Fyodor
What is your surname? Dostoevsky
Your new name is Theodore Dustworthy.Notice what has happened. The inputs to Americanize were not merely consulted by the procedure; they were changed. At the time the variables First and Second were passed to the procedure, they were initialized to 'Fyodor' and 'Dostoevsky'. After processing in the procedure, the same variables had acquired the new values, 'Theodore' and 'Dustworthy'.
This did not happen in the DrawBox procedure. The parameters we passed to it (e.g. 5,6,7,8) were unchanged by the procedure; they were merely consulted or used.
Whether a procedure is allowed to change its parameters, or only consult them, is up to us. If we pass the procedure a variable, then it may change its value with a new assignment statement. If we pass it a value, then it cannot change it, since only variables can take new assignments. So naturally we call the first type variable parameters and the second type value parameters. Americanize used variable parameters, while DrawBox used value parameters.
We indicate to the compiler that we want value or variable parameters in the way we declare the procedure. I've copied DrawBox's procedure header from example (9) above, and Americanize's header from example (13) above, for comparison:
procedure DrawBox(UpLeftX,UpLeftY,Width,Height : integer);
{value parameters}
procedure Americanize(var FirstName, Surname : string);
{variable parameters}In each case, the parameters are listed in parentheses, separated by commas, and followed by a colon and their type. But in Americanize, the variable identifiers are preceded by the reserved word var. That is the significant difference that makes the Americanize parameters variable rather than value parameters.
Here are a few more technical points on declaring parameters. (Skip these on first reading.)
- Just as in declaring variables in a program, you can link many identifiers with commas, and use the same colon for all of them, if they are of the same type. You can say
DrawBox(UpLeftX,UpleftY,Width,Height : integer);
instead of
DrawBox(UpLeftX:integer; UpLeftY:integer; Width:integer; Height:integer);
- However, you cannot link value and variable parameters in the same list by commas, even if they are of the same type. For example:
SampleProcedure(Width,Height, var Beauty:integer); {no}
Sample Procedure(Width,Height:integer; var Beauty:integer); {yes}
- If you have parameters of different types, separate them by semi-colons:
SampleProcedure(Width,Height:integer, Name:string); {no}
SampleProcedure(Width,Height:integer; Name:string); {yes}
- Remember that Pascal is a free-format language. Break the parameter list into several lines if necessary for clarity. Use indentation and comments to help humans understand what is going on. For example,
procedure DrawBox
(UpLeftX, {horizontal position of upper left corner}
UpLeftY, {vertical position of upper left corner}
Width, {width of box, including the sides}
Height {height of box, including top and bottom}
: integer);
- If the parameter list of a procedure looks like the declaration of variables, then there is a good reason: it is the declaration of variables. The variables in the parameter list --whether preceded by var or not-- need not be declared anywhere else. They need not be declared as local variables in that procedure or globally.
- However, the variables (if any) used in a procedure call must be declared before they can be used, like any other variables. If the call occurs in another procedure, they should be declared locally in that calling procedure; otherwise they must be globally declared. But the declaration in the parameter list of the procedure it calls does not cover the variables in the call itself; the reason is simply that the call occurs elsewhere in the program, outside the scope or block of the called procedure.
Two kinds of parameter, continued
Here's a more detailed elaboration of the distinction between value and variable parameters. If this is not crystal clear on first reading, don't worry. Writing some procedures to test or illustrate these claims, or to take advantage of the powers they describe in programs that do interesting work, is essential before you can expect to feel comfortable with the details.
In DrawBox, there are locally declared variables for the [x,y] coordinates of the upper left corner of the box, its width, and its height --namely, those declared in DrawBox's parameter list. These local variables are set to the values passed to it by the arguments in the procedure call. As it happens, DrawBox does not assign to those variables yet other values in the course of its run. But it could. However, even if it assigned those local variables new values a dozen times, it would not change the arguments in the procedure call. Nor would it change the parameters in DrawBox's parameter list. DrawBox cannot change the value 5 to anything else. It can set one of its variables to 5 and later reset that variable to another value. But the value 5 originally passed to it is unaffected by that. So when we pass mere value parameters, no action by the procedure can affect the arguments or parameters of the procedure.
In DrawBox we passed the actual numbers, 5, 6, 7, and 8. But we might have passed expressions that evaluated to these numbers. For example
(16)DrawBox(UserX,UserY,UserWidth,UserHeight);
{variables presumably set to the values we want, as opposed
to tokens for those values themselves.}(17)DrawBox(1,1,Width,(Width*2)); {a mix of tokens, variables, and operations}
These expressions are evaluated when they are passed to DrawBox, and their values are assigned to the local variables in DrawBox declared in the DrawBox parameter list. Even if we use expressions in this way, we are still using value parameters. If the assignment statements inside DrawBox only use local variables, then they cannot affect the variables in our procedure call.
(Technical aside. We can use any kind of expression in the procedure call for a value parameter. But we can only use variables in the procedure call for a variable parameter.)
In Americanize, we pass variables, not just their values. So assignment statements inside Americanize can affect those variables everywhere in the program, in particular, in the neighborhood of the procedure call.
But how can this be? In the procedure call, we used the variables First and Second. But in declaring Americanize, we used the variables FirstName and Surname. How can local assignments to FirstName affect First, and local assignments to Surname affect Second? The answer is that when TP sees that we are using variable parameters (because we used the reserved word var in the procedure header), it gives the same variable two names. One name is used as an argument in the procedure call; the other name is used in the parameter list in the procedure's declaration. More simply, one name is for the procedure call, and the other is for the called procedure. Because they name the same variable, assignments to the procedure's "local variable" are actually assignments to the "local name" of a global variable. Hence, the effect of the assignment can be felt outside the procedure.
When would we want the effect of a local assignment to have global consequences? Answer: whenever we want two-way communication between a procedure call and a procedure. In DrawBox there was only one-way communication. The procedure call gave input to the procedure, but the procedure did not "reply" to the procedure call. In Americanize we had two-way communication. The procedure call sent 'Fyodor' 'Dostoevsky', and the procedure sent back 'Theodore' 'Dustworthy'. The variables used in the procedure call had one set of values before the procedure was executed, and another set afterwards. The procedure "talks back" to the procedure call with those new values. As you saw above, we can use those new values immediately after the procedure call --when we printed First and Second right after the procedure call and got 'Theodore' 'Dustworthy' instead of 'Fyodor' 'Dostoevsky'.
This should suggest that there are three levels of communication with a procedure. First: no communication. We pass no parameters at all. We just call it and have it perform the actions collected inside it. SoundEffects is that kind of procedure. So is TogglePrinterBegin in the file Library.pas. Second: talk to it, but don't expect it to talk back. Send it value parameters. Send it information to consult, and act upon, but not variables to change. No variables local to the procedure call will have new values as a result of the procedure's execution. Third: give it input and get output for use in the block of the procedure call. Pass variable parameters. Send the variable itself, and let the procedure reassign its values before quitting. (I owe this three-tiered perspective to John Howell.)
You might object: I can see why passing a global variable might be useful, since the procedure could change its value and thereby "talk back" to the part of the program where the procedure call occurred. But why not do this with ordinary global variables that have only one name? Why use these strange global variables with two names?
The answer is that if variable parameters had only one name, we'd have to know what those names were when we write the procedure call. In the case of Americanize, we'd have to know that it used FirstName and Surname instead of First and Second. By letting variable parameters have two names, we don't have to know what name is used internally by the procedure when we are writing a procedure call. At first the advantage seems small: we don't have to scroll around the file (or in large programs, the set of files) looking for the procedure declaration. But in practice the advantage is large: we don't have to know anything about the procedure's internal workings (except that the first parameter corresponds to the first name and the second to the surname). It can be a "black box" to us. This helps us think about our program at a higher level of abstraction; it helps us forget details and think about structure. It helps us write modular code whose parts interact with each other without entanglements that threaten their autonomy and individuality; we can still maintain and revise the code by working part by part in isolation from the rest.
(Does this answer help you answer the challenge posed earlier: why Pascal scope rules enforce local precedence instead of global precedence?)
We can mix value and variable parameters in the same procedure. Suppose we've written a program with many variables corresponding to the students in a class, and their grades on each piece of homework. We might write a procedure called "Summarize" that returned useful information about a particular assignment.
(18)Summarize(Assignment,High,Low,Average);
The variable, Assignment, would be initialized to the assignment about which we wanted a summary, e.g. 'FirstQuiz'. But the variables, High, Low, and Average, might be initialized at random, for all we care. Only after the procedure runs would they take values of interest, namely, the high score in class, the low score, and the average score.
Since we want High, Low, and Average to change as a result of the procedure execution, they must be variable parameters. Since we don't want Assignment to change during the procedure run, it should be a value parameter. Here's how Summary's header might look:
(19)Summarize(Assignment:string; var High,Low,Average:real);
High, Low, and Average are preceded by the reserved word var, indicating that they are variable parameters. Assignment is not, indicating that it is a value parameter.
(20)writeln(Assignment,High,Low,Average);
{permissible but dumb; the values of the last three}
{variables might be inaccurate; we should call Summarize}
{first to update them. Besides the values will be}
{displayed in the default field width in floating point}
{notation. Ugh!}
Summarize(Assignment,High,Low,Average);
writeln(Assignment, High:7:2, Low:7:2, Average:7:2);
{This is better. The printed values are up to date.}
{If we used value parameters they would not be updated}
{in this block of the program, but only inside Summarize.}
{By using variable parameters, the new values are stored}
{in the variables of the procedure call as soon as the}
{procedure runs. And finally, but not least...}
{our real numbers are formatted.}One final example. Suppose we were revising a program written by someone else. It contains a procedure called BlackBox, but we're not allowed to see how it was declared. So we have no idea what it does. All we know is that it takes one integer parameter, and that during the course of its run it makes new assignments to the variable in its parameter list. We write this code to test it:
(21)amount := 5;
writeln(amount); {Obviously the screen will display 5}
BlackBox(amount);
writeln(amount); {Now what will the screen display?}
If the result of the second writeln is not 5, then we know that BlackBox uses a variable parameter. If it is 5, then BlackBox may use either a value or variable parameter. If you see why, then you understand what is most important about value and variable parameters.
This file is an electronic hand-out for the course, Programming and Problem Solving.
Peter Suber,
Department of Philosophy,
Earlham College, Richmond, Indiana, 47374, U.S.A.
peters@earlham.edu. Copyright © 1997, Peter Suber.