modula-2 home

  Home  
  Tutorial  
  Win32 API  
  Reference  
  Projects  
 

 

Chapter 5 - Modula-2 Procedures


In order to define the procedure, we will need to lay some groundwork in the form of a few definitions.

Program Heading - This is the easiest part since it is only one line, at least it has been in all of our programs up to this point. It is simply the MODULE line, and it never needs to be any more involved than it has been up to this point, except for one small addition which we will cover in a later chapter.

Declaration Part - This is the part of the Modula-2 source code in which all constants, variables, and other user defined auxiliary operations are defined. In some of the programs we have examined, there have been one or more VAR declarations and in one case a constant was declared. These are the only components of the declaration part we have used up to this time. There are actually four components in the declaration part, and the procedures make up the fourth part. We will cover the others in the next chapter.

Statement Part - This is the last part of any Modula-2 program, and it is what we have been calling the main program. It always exists bounded by the reserved words BEGIN and END just as it has in all of our examples to this point.

It is very important that you grasp the above definitions because we will be referring to them constantly during this chapter, and throughout the remainder of this tutorial. With that introduction, let us look at our first Modula-2 program with a procedure in it. It will, in fact, have three procedures.

What is a procedure?

A procedure is a group of statements, either predefined by the compiler writer, or defined by you, that can be called upon to do a specific job. In this chapter we will see how to write and use a procedure. During your programming in the future, you will use many procedures. In fact, you have already used some because the "WriteString", "WriteLn", etc procedures you have been using are procedures defined in a separate module InOut.

Load and display the program named PROCED1.MOD for your first look at a user defined procedure. In this program, we have the usual header with one variable defined. Ignore the header and move down to the main program beginning with line 25. We will come back to all of the statements prior to the main program in a few minutes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
MODULE Proced1;

FROM Terminal2 IMPORT WriteString, WriteLn;

VAR Count : INTEGER;

PROCEDURE WriteHeader;
BEGIN
   WriteString("This is the header");
   WriteLn;
END WriteHeader;

PROCEDURE WriteMessage;
BEGIN
   WriteString("This is the message");
   WriteLn;
END WriteMessage;

PROCEDURE WriteEnding;
BEGIN
   WriteString("This is the end");
   WriteLn;
END WriteEnding;

BEGIN        (* Main program *)
   WriteHeader;
   FOR Count := 1 TO 8 DO
      WriteMessage;
   END;
   WriteEnding;
END Proced1.

The main program is very easy to understand based on all of your past experience with Modula-2. First we somehow write a header (WriteHeader), then write a message out 8 times (WriteMessage), and finally we write an ending out (WriteEnding). Notice that with the long names for the parts, no comments are needed, the program is self documenting. The only problem we have is, how does the computer actually do the three steps we have asked for. That is the purpose for the 3 procedures defined earlier starting in lines 7, 13, and 19. Modula-2 requires that nothing can be used until it has been defined, so the procedures are required to be defined prior to the main program. This may seem a bit backward to you if you are experienced in some other languages like FORTRAN, BASIC, or C, but it will make sense eventually.

How do we define a procedure?

We will begin with the PROCEDURE at line 8. First we must use the reserved word PROCEDURE followed by the name we have chosen for our procedure, in this case "WriteHeader" which is required to follow all of the rules for naming an identifier. Following the PROCEDURE line, we can include more IMPORT lists, define variables, or any of several other things. We will go into a complete definition of this part of the program in the next chapter. I just wanted to mention that other quantities could be inserted here. We finally come to the procedure body which contains the actual instructions we wish to execute in the procedure. In this case, the procedure body is very simple, containing only a "WriteString" and a "WriteLn" instruction, but it could have been as complex as we needed to make it.

At the end of the procedure, we once again use the reserved word END followed by the same name as we defined for the procedure name. In the case of a procedure, the final name is followed by a semicolon instead of a period. Other than this small change, a procedure definition is identical to that of the program itself.

When the main program comes to the "WriteHeader" statement, it knows that it is not part of its standard list of executable instructions, so it looks for the user defined procedure by that name. When it finds it, it transfers control of the program sequence to there, and begins executing those instructions. When it executes all of the instructions in the procedure, it finds the END statement of the procedure and returns to the next statement in the main program. When the main program finally runs out of things to do, it finds the END statement and terminates.

As the program executes the FOR loop, it is required to call the "WriteMessage" procedure 8 times, each time writing its message on the monitor, and finally it finds and executes the "WriteEnding" procedure. This should be very straightforward and should pose no real problem for you to understand. When you think you understand what it should do, compile and run it to see if it does.

Now for a procedure that uses some data

The last program was interesting to show you how a procedure works but if you would like to see how to get some data into the procedure, load and display the program named PROCED2.MOD. We will once again go straight to the program starting in line number 24. We immediately notice that the program is nothing more than one big FOR loop which we go through 3 times. Each time through the loop we call several procedures, some that are system defined, and some that are user defined. This time instead of the simple procedure name, we have a variable in the parentheses behind the variable name. In these procedures, we will take some data with us to the procedures, when we call them, just like we have been doing with the "WriteString" and "WriteInt" procedures.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
MODULE Proced2;

FROM Terminal2 IMPORT WriteString, WriteInt, WriteLn;

VAR Stuff : INTEGER;
    Thing : INTEGER;

PROCEDURE PrintDataOut(Puppy : INTEGER);
BEGIN
   WriteString("The value of Puppy is     ");
   WriteInt(Puppy,5);
   WriteLn;
   Puppy := 12;
END PrintDataOut;

PROCEDURE PrintAndModify(VAR Cat : INTEGER);
BEGIN
   WriteString("The value of Cat is       ");
   WriteInt(Cat,5);
   WriteLn;
   Cat := 37;
END PrintAndModify;

BEGIN        (* Main program *)
   FOR Stuff := 3 TO 5 DO
      Thing := Stuff;
      PrintDataOut(Thing);
         WriteString("Back from print, data is  ");
         WriteInt(Thing,5);
         WriteLn;
      PrintAndModify(Thing);
         WriteString("Back from modify, data is ");
         WriteInt(Thing,5);
         WriteLn;
      PrintDataOut(Thing);
         WriteString("Back from print, data is  ");
         WriteInt(Thing,5);
         WriteLn;
         WriteLn;
   END;
END Proced2.

We will take some data to the procedure named "PrintDataOut" where it will be printed. The procedure "PrintDataOut" starting in line 8 also contains a pair of parentheses with a variable named "Puppy" which is of type INTEGER. This says that it is expecting a variable to be passed to it from the calling program and it expects the variable to be of type INTEGER. Back to the main program we see, on line 27, that the program did make the call to the procedure with a variable named "Thing" which is an INTEGER type variable, so everything is fine. The procedure prefers to call the variable passed to it "Puppy" but that is perfectly acceptable, it is the same variable. The procedure writes the value of "Puppy", which is really the variable "Thing" in the main program, in a line with an identifying string, then changes the value of "Puppy" before returning to the main program.

Upon returning to the main program, we print out another line with three separate parts (notice the indenting and the way it makes the program more readable), then calls the next procedure "PrintAndModify" which appears to do the same thing as the last one. Indeed, studying the procedure itself leads you to believe they are the same, except for the fact that this one prefers to use the name "Cat" for a variable name. There is one subtle difference in this procedure, the reserved word VAR in the header, line 16.

Call by value & call by reference

In the first procedure, the word VAR was omitted. This is a signal to the compiler that this procedure will not actually receive the variable reference, instead it will receive a local copy of the variable which it can use in whatever way it needs to. When it is finished, however, it can not return any changes in the variable to the main program because it can only work with its copy of the variable. This is therefore a one-way variable, it can only pass data to the procedure. This is sometimes called a "call by value" or a "value parameter" in literature about Modula-2.

In the second procedure, the word VAR was included. This signals the compiler that the variable in this procedure is meant to be actually passed to the procedure, and not just the value of the variable. The procedure can use this variable in any way it desires, and since it has access to the variable in the main program, it can alter it if it so desires. This is therefore a two-way variable, it can pass data from the main program to the procedure and back again. This is sometimes called a "call by reference" or a "variable parameter" in literature about Modula-2.

Which should be used?

It is up to you to decide which of the two parameter passing schemes you should use for each application. The "two-way" scheme seems to give the greatest flexibility, so your first thought is to simply use it everywhere. But that is not a good idea because it gives every procedure the ability to corrupt your main program variables. In addition, if you use a "call by value" in the procedure definition, you have the ability to call the procedure with a constant in that part of the call. A good example is given in lines 11, 19, 29, 33, and 37 of the present program. If "WriteInt" were defined with a "call by reference", we could not use a constant here, but instead would have to set up a variable, assign it the desired value, then use the variable name instead of the 5. There are other considerations but they are beyond the level of our study at this point.

Back to the program on display, Proced2

We have already mentioned that both of the procedures modify their respective local variables, but due to the difference in "call by value" in the first, and "call by reference" in the second, only the second can actually get the modified data back to the calling program. This is why they are named the way they are. One other thing should be mentioned. Since it is not good practice to modify the variable used to control the FOR loop, (and downright erroneous in many cases) we make a copy of it and call it "Thing" for use in the calls to the procedures. Based on all we have said above, you should be able to figure out what the program will do, then compile and run it.

Several parameters passed at once

Load and display the program named PROCED3.MOD for an example of a procedure definition with more than one variable being passed to it. In this case four parameters are passed to this procedure. Three of the parameters are one-way and one is a two-way parameter. In this case we simply add the three numbers and return it to the main program. Good programming practice would dictate the placement of the single "call by reference" by itself and the others grouped together, but it is more important to demonstrate to you that they can be in any order you desire. This is a very straightforward example that should pose no problem to you. Compile and run it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MODULE Proced3;

FROM Terminal2 IMPORT WriteString, WriteCard, WriteLn;

VAR Apple, Orange, Pear, Fruit : CARDINAL;

PROCEDURE AddTheFruit (Value1,Value2 : CARDINAL;  (* One-way *)
                       VAR Total     : CARDINAL;  (* Two-way *)
                       Value3        : CARDINAL); (* One-way *)
BEGIN
   Total := Value1 + Value2 + Value3;
END AddTheFruit;

BEGIN  (* Main Program *)
   Apple := 4;
   Orange := 7;
   Pear := 5;
   AddTheFruit(Apple,Pear,Fruit,Orange);
   WriteString("The total number of fruits is ");
   WriteCard(Fruit,5);
   WriteLn;
END Proced3.

Scope of variables

Load and display the program PROCED4.MOD for a program which can be used to define scope of variables or where variables can be used in a program. The two variables defined in lines 5 and 6, are of course available in the main program because they are defined prior to it. The two variables defined in the procedure are available within the procedure because that is where they are defined. However, because the variable "Count" is defined in both places, it is two completely separate variables. The main program can never use the variable "Count" defined in the procedure, and the procedure can never use the variable "Count" defined in the main program. They are two completely separate and unique variables with no ties between them. This is useful because when your programs grow, you can define a variable in a procedure, use it in whatever way you wish, and not have to worry that you are corrupting some other "global" variable. The variables in the main program are called "global variables" because they are available everywhere.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
MODULE Proced4;

FROM Terminal2 IMPORT WriteString, WriteCard, WriteLn;

VAR Count : CARDINAL;
    Other : CARDINAL;

PROCEDURE PrintSomeData;
VAR Count : CARDINAL;
    Apple : CARDINAL;
BEGIN
   Count := 7;   (* This is the variable Count defined
                    locally in this procedure *)
   Other := 12;
   Apple := 32;
   WriteString("In PrintSomeData the variables are");
   WriteCard(Count,5);
   WriteCard(Other,5);
   WriteCard(Apple,5);
   WriteLn;
END PrintSomeData;

PROCEDURE Main; (* Main program *)
VAR Index : CARDINAL;
BEGIN
   FOR Index := 1 TO 3 DO
      Count := Index;
      Other := Index;
         WriteString("In Main Program the variables are ");
         WriteCard(Index,5);
         WriteCard(Count,5);
         WriteCard(Other,5);
         WriteLn;
      PrintSomeData;
         WriteString("In Main Program the variables are ");
         WriteCard(Index,5);
         WriteCard(Count,5);
         WriteCard(Other,5);
         WriteLn;
      WriteLn;
   END;  (* of FOR loop *)
END Main;

BEGIN
   Main;
END Proced4.

In addition to the above scope rules, the variable named "Apple" in the procedure, is not available to the main program. Since it is defined in the procedure it can only be used in the procedure. The procedure effectively builds a wall around the variable "Apple" and its own "Count" so that neither is available outside of the procedure. We will see in the next chapter that procedures can be "nested" leading to further hiding of variables. This program is intended to illustrate the scope of variables, and it would be good for you to study it, then compile and run it.

A procedure can call another procedure

Load and display the program named PROCED5.MOD for an example of procedures that call other procedures. Study of this program will reveal that procedure "Three" starting on line 19 calls procedure "Two" which in turn calls procedure "One". The main program calls all three, one at a time, and the result is a succession of calls which should be rather easy for you to follow. The general rule is, "any program or procedure can call any other procedure that has been previously defined, and is visible to it." (We will say more about visibility later.) Study this program then compile and run it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
MODULE Proced5;

FROM Terminal2 IMPORT WriteString, WriteLn;

PROCEDURE One;
BEGIN
   WriteString("This is procedure One.");
   WriteLn;
END One;

PROCEDURE Two;
BEGIN
   One;
   WriteString("This is procedure Two.");
   WriteLn;
END Two;

PROCEDURE Three;
BEGIN
   Two;
   WriteString("This is procedure Three.");
   WriteLn;
END Three;

BEGIN   (* Main program *)
   One;
   WriteLn;
   Two;
   WriteLn;
   Three;
   WriteLn;
END Proced5.

A function procedure

Load and display the program named FUNCTION.MOD for an example of a "Function Procedure". This contains a procedure very much like the ones we have seen so far with one difference. In the procedure heading, line 5, there is an added ": INTEGER" at the end of the argument list. This is a signal to the system that this procedure is a "function procedure" and it therefore returns a value to the calling program in a way other than that provided for by parameter references as we have used before. In fact, this program returns a single data value that will be of type INTEGER.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MODULE Function;

FROM Terminal2 IMPORT WriteString, WriteInt, WriteLn;

PROCEDURE QuadOfSum(Number1, Number2 : INTEGER) : INTEGER;
BEGIN
   RETURN(4*(Number1 + Number2));
END QuadOfSum;

VAR Dogs, Cats, Feet : INTEGER;

BEGIN  (* Main program *)
   Dogs := 4;
   Cats := 3;
   Feet := QuadOfSum(Dogs,Cats);
   WriteString("There are a total of");
   WriteInt(Feet,3);
   WriteString(" paws.");
   WriteLn;
END Function.

In line 15 of the calling program, we find the call to the procedure which looks like the others we have used except that it is used in an assignment statement as though it is an INTEGER type variable. This is exactly what it is and when the call is completed, the "QuadOfSum(Dogs,Cats)" will be replaced by the answer and then assigned to the variable "Feet". The entire call can therefore be used anyplace in a program where it is legal to use an INTEGER type variable. This is therefore a single value return and can be very useful in the right situation. In one of the earlier program, we used the "sin" and "cos" function procedures and this is exactly what they were.

One additional point must be made here. If a function procedure does not require any parameters, the call to it must include empty parentheses, and the definition of the procedure must include empty parentheses also. This is by definition of the Modula-2 language.

In the procedure, we had to do one thing slightly different in order to get the return value and that was to use the RETURN reserved word. Whenever we have completed the desired calculations or whatever we need to do, we put the result that is to be returned to the main program in the parentheses following the RETURN and the procedure will terminate, return to the calling program, and take the value with it as the answer. Due to decision making, we may have several RETURN statements in the procedure but only one will be exercised with each call. It is an error to come to the END statement of a function procedure since that would constitute a return without the benefit of the RETURN statement, and no value would be returned to the calling program.

What is recursion?

Recursion is simply a procedure calling itself. If you have never been introduced to recursion before, that definition sounds too simple but that is exactly what it is. You have probably seen a picture containing a picture of itself. The picture in the picture also contains a picture of itself, the end result being an infinity of pictures. Load the file named RECURSION.MOD for an example of a program with recursion.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MODULE Recursion;

FROM Terminal2 IMPORT WriteString, WriteInt, WriteLn;

VAR Count : INTEGER;

PROCEDURE PrintAndDecrement(Index : INTEGER);
BEGIN
   WriteString("The value of the Index is");
   WriteInt(Index,5);
   WriteLn;
   Index := Index - 1;
   IF Index > 0 THEN
      PrintAndDecrement(Index);
   END;
END PrintAndDecrement;

BEGIN    (* Main program *)
   Count := 7;
   PrintAndDecrement(Count);
END Recursion.

In the main program, "Count" is set to 7 and the procedure is called taking along "Count" as a parameter. In the procedure, we display a line containing the value of the variable, now called "Index", and decrement it. If the variable is greater than zero, we call the same procedure again, this time entering it with the value of 6. It would be reasonably correct to think of the system as creating another copy of the procedure for this call. The variable "Index" would be reduced to 5, and another copy of the procedure would be called. Finally, the variable would be reduced to zero and the return path from procedure to procedure would be taken until the main program would be reached, where the program would terminate.

Local variables (variables that are local to procedures) are stored in an area in memory called "the stack". The essence of a stack is that you always add new items on top and always remove items beginning from the top, so there is never a "hole" within the stack. When a function is called, its local variables are allocated on the top of the stack (and the stack becomes larger); when it ends, its variables are deallocated (and the stack becomes smaller). Usually, you have no need to worry about this, because it is all taken care of for you by the system. However, when placing a lot of data on the stack, which can easily happen when you use recursion, the stack will eventually collide with other memory areas, often resulting in a computer crash. Therefore, when using recursion, you must make sure that there is some mechanism by which the process of recursion will terminate.

Recursion can be very useful for those problems that warrant its use. This example is a very stupid use of recursion, but is an excellent method for giving an example of a program with recursion that is simple and easy to understand.

Direct and indirect recursion

This example uses direct recursion because the procedure calls itself directly. It is also possible to use indirect recursion where procedure "A" calls "B", "B" calls "A", etc. Either method is available and useful depending on the particular circumstances.

Programming exercises

  1. Write a program to write your name, address, and phone number on the monitor with each line in a different procedure.
  2. Add a statement to the procedure in RECURSION to display the value of "Index" after the call to itself so you can see the value increasing as the recurring calls are returned to the next higher level.
  3. Rewrite TEMPCONV from chapter 4 putting the centigrade to farenheit formula in a function procedure.

 

Next: Chapter 6. Arrays, Types, Constants