Modula-2 allows almost anything to be a variable type. (Exceptions include modules and types themselves). It is not by any means unique in this respect, but shares this ability with several other languages. What is allowed in Modula-2, though, and not in many others, is the assignment of procedures to a variable. That is, variables can be of a procedure type. As before, this is done by specifying the existence of the type first, and in this case, the parameter list types also form part of the type declaration.
There are several possible uses for this particular facility. One of the simplest is to accommodate the differences in the names of imported procedures in various versions. Suppose, for instance, one were to write a large program that contained the line:
FROM STextIO IMPORT WriteChar;
and there were several hundred references to WriteChar throughout the program. Later, it was desired to run the same program on a different machine where there were a similar module and procedure, but named InOut and Write, respectively, with InOut.Write having the same semantics as STextIO.WriteChar. The line above must be revised to:
FROM InOut IMPORT Write;
However, that is not enough. There are still all those references to WriteChar scattered through the program. For the program to work correctly (or even compile), one must evidently change them all to Write. But, how to ensure they are all done? And, how to change them all back again if, after further development and change, the first machine is to get the benefit of the changes as well? If the entity in question were a variable, all one would have to do is place an assignment statement at the beginning of the program. In Modula-2, one can do the same thing with procedures. All that is necessary is the declarations:
TYPE WriteProc = PROCEDURE (VAR CHAR); VAR WriteChar : WriteProc;
to establish a variable of the appropriate procedure type, and the line of code:
WriteChar := Write;
somewhere near the start of the main program, and all will be well. Now, the calls to WriteChar are translated into calls to the procedure this variable equals. If it is necessary to port a new version of this second program back to the original system, the import line can be changed and the two lines of declaration and the assignment perhaps commented out.
A second (more complex) example involves the assignment of procedures in order to improve the flow of a program itself. For instance, if one wishes to offer the user a choice of calculating the area and perimeter of either a square or a circle, one could offer a menu and then call one or the other of the procedures. Alternately, one can, using the facility here, set an appropriate procedure variable. This is realized below:
MODULE ProcVarDemo; FROM STextIO IMPORT WriteString, WriteLn, ReadChar, SkipLine; FROM SRealIO IMPORT ReadReal, WriteReal; TYPE AreaPerim = PROCEDURE (REAL, VAR REAL, VAR REAL); (* Specify only the types of the variables in the parameter list when declaring a procedure Type. Every variable parameter must be individually specified as such *) VAR SizeCalc : AreaPerim; dimension, area, perimeter : REAL; which : CHAR; PROCEDURE Squares (side : REAL; VAR area, perim : REAL); BEGIN area := side * side; perim := 4.0 * side; END Squares; PROCEDURE Circles (radius : REAL; VAR area, circ : REAL); CONST pi = 3.14159265; twopi = 2.0 * pi; BEGIN area := pi * radius * radius; circ := twopi * radius; END Circles; PROCEDURE GetNum ( ) : REAL; VAR numToGet : REAL; BEGIN WriteString ("Please type in the figure size here ==> "); ReadReal (numToGet); SkipLine; WriteLn; RETURN numToGet; END GetNum; BEGIN (* Main program starts here. *) WriteString ("I will calculate the area and perimeter "); WriteLn; WriteString ("of either a circle or a square."); WriteLn; WriteString ("Type a 'C' or an 'S' here ===> "); ReadChar (which); SkipLine; WriteLn; (* assign procedure variable to correct procedure *) IF CAP (which) = 'C' THEN SizeCalc := Circles; ELSE SizeCalc := Squares; END; (* if *) SizeCalc (GetNum( ), area, perimeter); WriteString ("The area is "); WriteReal (area, 15); WriteLn; WriteString ("And, the perimeter is "); WriteReal (perimeter, 15); WriteLn; END ProcVarDemo.
One could of course leave out the TYPE part of the declaration and simply write:
VAR SizeCalc : PROCEDURE (REAL, VAR REAL, VAR REAL);
but as usual it is better to create a new type and then to use that type when declaring variables. This practice makes program modifications simpler and the text file becomes more readable and easier to follow.
Observe that the parameter type for each formal parameter must be separately specified, and the VAR must be indicated for each separately.
Modula-2 anticipates that procedure variables will be frequently used, and even has a built-in type PROC that denotes a parameterless procedure. (There are too many possibilities for procedure types that do take parameters to have any other procedure types built in.)
One can therefore assume that the language already has a definition equivalent to:
TYPE PROC = PROCEDURE ( ); (* no parameters *)
In other words, one can write
VAR Proc1, Proc2, Proc3 : PROC;
instead of having to write:
VAR Proc1, Proc2, Proc3 : PROCEDURE ( );
There have not been very many parameterless procedures in this text to date; however, they do play an important role in coprocesses and coroutines in Modula-2, and will surface several times later in the book. It is also worth noting that the assignment of procedure variables is distinguished from their invocation (calling) by the absence of a parameter list in the former case and its presence in the latter. Thus
Proc := Proc2
is an assignment of the procedure Proc2 to the procedure variable Proc, whereas
x := Proc2 (<actual parameters>)
is an invocation of the function procedure Proc2, and assigns the returned value to x.
This is true even if the parameter list is empty, so such empty parameter lists must always be given when the intention is to call the procedure, in order to avoid confusion. Thus, if x is a numeric variable, the statement
x := Proc2
would be interpreted as an attempt to assign the procedure Proc2 to the variable x and a type compatibility error would be generated.
NOTES: 1. Only procedures that are declared at the outermost level of a compilation unit can be assigned to procedure variables, not ones that are declared inside other procedures at level one or higher.
2. Standard procedures, such as ABS, cannot be assigned, though one can get around this (if one is a good-for-nothing hacker like the author) by enclosing the standard procedure inside another one with a slightly different name, and assigning the name of the procedure variable the enclosing procedure rather than (directly) the standard one. It is rather unlikely that many people would need to exploit this loophole.