Part A--Scope and Visibility Issues

10.2 Blocks, Global and Local Variables, Side Effects

Notations like Modula-2 and Pascal are sometimes called block-structured languages. What is meant by this is that code is organized or grouped into named sequences, called blocks. (In some languages, blocks do not need names, but they do in Modula-2). Both modules and procedures contain blocks, and these blocks provide the framework for organizing (structuring) a program. In general, the entire code following the line containing the name of the module or procedure is its main block, though subsidiary procedures inside it may contain other blocks.

A Modula-2 block consists of the declaration and body parts of a module or a procedure. It is declared by a heading that includes the block name. In the case of a procedure it also includes the parameter list. It contains
(1) a declaration section--definitions of entities to be used inside the block
(2) a statement sequence known as the block body
BEGIN
  statement sequence
END <block name>

As already seen, blocks must have names in Modula-2, and the identifier must be named in the first line (right after the reserved word MODULE or PROCEDURE, as the case may be), and also on the last line, right after the reserved word END.

A procedure may legally be written without a body:

  PROCEDURE Disembodied;
  END Disembodied;

but it would hardly be of any use. However, it may well happen that one would write an implementation part of a library module with no body, for it may have no need to do initializations of internal or exported items. The definition part of a library module is not permitted to contain a body.

A more substantial difference between modules and procedures is that the headings of procedures include parameter lists, and this is not applicable to the headings of modules. Module blocks, on the other hand may be preceded in the module by import lists, and these are not relevant to procedures.

The general purpose of blocks is to assist in structuring code for ease of organization. They also assist in segregating variables and other entities for use in specific parts of a program and, to contain code that may be used repeatedly in various contexts.

10.2.1 Procedure Blocks and Scope

As indicated in chapter three, any variables, constants, or other entities declared in a procedure have no existence whatsoever outside the scope of that procedure. Any attempt to refer to such identifiers in the main program will result in an identifier not declared error or some similar message being delivered by the compiler.

On the other hand, any variables that are declared in the scope surrounding a procedure--whether it be the main program module or another procedure--are available to all the procedures contained inside that scope. This is true at every level, so that if one had:

MODULE Visible;

VAR
  firstReal : REAL;

  PROCEDURE DoOne;

  VAR
    secondReal : REAL;

    PROCEDURE DoTwo;

    VAR
      thirdReal : REAL;

    BEGIN
      (* Body of DoTwo *)
    END DoTwo;
  

  BEGIN
    (* Body of DoOne *)
  END DoOne;

BEGIN
  (* Body of Visible *)
END Visible.

then the procedure DoTwo can use DoOne and all three of firstReal, secondReal, and thirdReal. procedure DoOne can use itself, DoTwo, firstReal and secondReal, but not thirdReal. The main program module can use only DoOne and just firstReal of the variables.

As far as any statement in the main module is concerned, the entities DoTwo, secondReal and thirdReal do not even exist. Whenever a procedure is entered, its own particular variables (whether formal parameters or declared in a VAR statement) come into existence and can have values assigned to them. As soon as this section is exited they cease to exist altogether, and the memory locations that they named are reclaimed for other uses by the program. The following summarizes these concepts in a couple of formal definitions :

An entity named in the parameter list of a procedure or in the declaration part of its block is said to be local to that block. The same entity is global to any block inside the procedure in which it is declared.

Thus in the module Visible, secondReal is local to DoOne and global to DoTwo. Some entities are more local than others. In fact, it is technically more correct to assert that Modula-2 does not have entirely global entities, just ones that are local to the outer block, because a program module executes in exactly the same manner as does one of its procedures. Perhaps it is best to refer to the surroundings and observe that an entity is local in the block in which it is declared, and global to any block contained within that. This is illustrated in figure 10.2.

The concept of locality applies not just to variables, but to constants, modules, procedures, and other entities as well. Clearly, it is important to know where in a program a given entity is in fact usable. Consider the following definitions:

If an entity (variable, constant, procedure, etc.) is available for use in a statement in some portion of a program, one says that it is visible in that portion of the program.
The scope of an entity is the portion of a program in which it is visible. Modules and procedures both define a scope for their entities, and an entity is visible in any procedure scope that is contained inside the one in which it is defined.

NOTES: 1. An entity is visible throughout the entire scope within which it is defined, not just from the point at which it is declared onward, as in some versions of Pascal. This characteristic of Modula-2 may have been sacrificed if the implementation you are using has a one-pass compiler.

2. The scope of a procedure starts immediately after its own name, and includes its formal parameters.

10.2.2 Side effects and Counting Loops

In general, it is a poor programming practice to declare all of a program's variables globally to the entire program. The larger the program is, the more likely this is to cause difficulties in keeping track of all the variables and their use. Failure to do so may result in writing code that modifies the value of a variable that is important to the correct functioning of some other part of the program. This difficulty increases approximately with the square of the size of the program, and tends to be particularly acute with procedures, because these usually have variables of their own, and may be written long after the main program at a time when the creator of the original code has either departed her employment or forgotten what its variables were for. Given this, it is the product of a moment's carelessness or ignorance to write a procedure that, besides its intended action, also modifies some important program variable.

The modification by a procedure of some variable global to it is called a side effect of the procedure.

Observe that some side effects are desirable, and may furnish the very reason for writing the procedure in the first place. For instance, an invocation of ReadChar(ch) is made for the express purpose of changing the value of global variable ch. On the other hand, some side effects are quite deleterious. For instance, novice programmers, finding that they have many procedures containing counting loops such as:

WHILE count < 10
  DO
...
   INC (count)
  END;

may be tempted to declare a single variable count at the outermost level of a program and allow it to be shared by all procedures.

If this is done, then whatever the declared purpose of one of these procedures, its action has the additional side effect of changing count. On some occasions, this is not be a problem, but what if one of these procedures calls another?

  
VAR
  count: CARDINAL;
PROCEDURE DoOne ( );
BEGIN
  count := 1;
  WHILE count < 4
    DO
      DoTwo;
      INC (count);
    END (* while *)
END DoOne;

PROCEDURE DoTwo ( );
BEGIN
  count := 12;
  WHILE count > 5
    DO
      Statement Sequence;
      DEC (count, 2);
    END (* while *)
END DoTwo;

What happens to the value of count that DoOne must maintain in order to execute correctly once DoTwo begins its own loop? An invocation of DoOne starts by setting count to 1 and starting up DoTwo, which then sets that same global variable count to 12 before cycling it through 12,10,8,6, and 4. When DoOne resumes control, it finds its loop control variable is now set to 4, so it will also exit--without performing the rest of its loop. This would be a difficult bug to locate, especially if the code for the two procedures were separated in the source file.

If the variable is the loop control variable in a FOR statement, this code would not be allowed, because such loop control variables must be declared in the same scope as they are used. That is, code such as

MODULE TestFor;

VAR
  count : CARDINAL; (* unused in code shown, but available. *)
  
PROCEDURE One;

BEGIN
FOR count := 1 TO 10 
  DO
  END;
END One;

PROCEDURE Two;

BEGIN
FOR count := 1 TO 10 
  DO
    One;
  END;
END Two;

END TestFor.

should produce error messages like

#    9  FOR count := 1 TO 10 
#####           ^ 205: illegal FOR variable
 File "TestFor.MOD"; Line 9
#   17  FOR count := 1 TO 10 
#####           ^ 205: illegal FOR variable
 File "TestFor.MOD"; Line 17

This is because it is an error to threaten the value of a loop control variable in a FOR loop, and this code certainly does that. Note, however, that some compilers do not take this error checking as seriously as they should, and may incorrectly ignore this error. The correct way to write such code is:

MODULE CorrectFor;

VAR
  count : CARDINAL; (* unused in code shown, but available *)
  
PROCEDURE One;
VAR
  count : CARDINAL;

BEGIN
FOR count := 1 TO 10 
  DO
  END;
END One;

PROCEDURE Two;
VAR
  count : CARDINAL;

BEGIN
FOR count := 1 TO 10 
  DO
    One;
  END;
END Two;

END CorrectFor.

Notice that here each procedure has its own variable count. A procedure may legally re-use an identifier that already exists in the surrounding scope. A variable called count in the main program, and one called count in a procedure contained inside the program, are different entities--they name different cells of the computer's memory. In this example, all three items called count have their own memory, independent of the others, and so do not interfere with the others.

The only effect this will have is that the original variable will not now be visible in the inner procedure. The name is, after all, being used locally. So, the multiple use of count causes no conflicts--all are distinct.

The non-threatening rule only applies to FOR loops, not to WHILE or REPEAT loops, but the side effect problem can be fixed the same way, by declaring variables that are local to the scope containing the loop.

The point is that side effects can easily destroy the validity of a program, and that such side effects should therefore be avoided. It follows that the best way to use procedures is to construct them as black boxes that take in certain input, produce well-defined output and do not otherwise affect the surrounding program through their inner workings.

10.2.3 Other Global side effects

Apart from the damage done to loop control variables, there are other deleterious side effects involving local modification of global variables. To illustrate, consider the following pathological example:

MODULE BadSideEffect;
FROM STextIO IMPORT
  WriteLn;
FROM SWholeIO IMPORT
  WriteCard;

CONST
  two = 2;
VAR
  result, global : CARDINAL;

PROCEDURE Func (local : CARDINAL) : CARDINAL;
BEGIN
  local := global + local;
  INC (global);
  RETURN local;
END Func;

BEGIN  (* main *)
  global := 5;
  result := Func (two) + Func (two);
  WriteCard (result, 0);
  WriteLn;
  global := 5;
  result := 2 * Func (two);
  WriteCard (result, 0);
  WriteLn;
END BadSideEffect.

When this program was run, the output was

15
14

Whoops! Surely it ought to be the case that any function procedure has the property that

Func(two) + Func(two) equals 2 * Func(two)

as it does in most reasonable mathematics, but here it does not. Here's why. When the statement

result := Func (two) + Func (two)

is executed, the procedure is entered twice. On first entry, global has the value 5, and on first exit the value returned by Func is 7 and that of global is now 6. The second call returns the function value 8. The sum of the two function results is 15. On the other hand, when the statement

result := 2 * Func (two)

is executed with global first set back to 5, the procedure is entered only once, returning 7; this is doubled; and 14 is output.

The example further illustrates that procedures should modify only local variables, not global ones. It also illustrates that a piece of code must be read for its meaning rather than simply for its form in order to understand the effect of executing that code.

Here are two suggestions for avoiding deleterious side effects of this kind:

1: Avoid manipulating global variables within local routines if possible. Pass more parameters instead.
2: Declare only those variables as global that are necessary. If only a few procedures will use a variable, it may not belong in the outer block.
3: Re-use names where appropriate; the compiler will know to employ a different memory cell.

10.2.4 Nested Procedure Scopes

As noted in passing above, when a name is re-used in an inner (nested) scope, all references to that name in the inner scope are to the entity declared in that scope, not to the global entity of the same name. The re-use of the name cuts off the access to the global entity of the same name. Here is another illustration:

MODULE Nested;
FROM STextIO IMPORT
  WriteLn;
FROM SRealIO IMPORT
  WriteReal;
VAR
  number1, number2 : REAL;

PROCEDURE DoOne;
VAR
  number1 : REAL;

  PROCEDURE DoTwo;
  VAR
    number1, number2 : REAL;
  BEGIN   (* for DoTwo *)
    number1 := 1.0;  (* no change to number1 in DoOne or Nested *)
    number2 := 2.0;  (* doesn't affect number2 in main program *)
  END DoTwo;

BEGIN   (* for DoOne *)
    number1 := 3.0;  (* no effect on number1 in Main program *)
    number2 := 4.0;  (* does affect number2 in main program *)
    DoTwo;
END DoOne;

BEGIN
  number1 := 5.0;
  number2 := 6.0;
  DoOne;
  WriteReal (number1, 5);
  WriteLn;
  WriteReal (number2, 5)
END Nested.

The output for the above module is

5.000
4.000

for number1 and number2 respectively, for when an assignment is made to a variable inside a procedure, it will be done with the locally defined entity of that name. No search outside the procedure will be instituted unless the identifier cannot found locally.

An entity declared in the main module is said to be at the outermost level, or level zero. An entity declared in a procedure belonging to the main module (one scope inside the main one) is at level one. An entity declared in a procedure nested n scopes inside the main scope is at level n.

Naturally, if two procedures are declared at the same level, they cannot make any use of each other's entities--including any procedures hidden inside each other. Figure 10.3 illustrates:

NOTE: There may be an implementation specific restrictions on the use of the procedure Three inside the procedure One because of declare-it-before-you-use-it restrictions. If that is the case, declare Three FORWARD as shown in section 4.8.

To illustrate the use of nested procedures, consider this little program that draws bar graphs on up to three quantities and up to 30 units wide. Observe the multiple use in different scopes of the counting variable and the use of subprocedures by a main procedure that do not need to be visible to the entire program (and so are not).

MODULE BarGraph;
(* by R. Sutcliffe
To illustrate nested procedures and scope
revised 1994 04 06 *)

FROM STextIO IMPORT
  WriteChar, WriteString, ReadChar, SkipLine, WriteLn;
FROM SWholeIO IMPORT
  ReadCard, WriteCard;
FROM SIOResult IMPORT
  ReadResult, ReadResults;
  
PROCEDURE DrawBar (wide : CARDINAL; desc : CHAR);
(* This procedure draws the entire bar using two subprocedures. *)

PROCEDURE MakeSide (howWide : CARDINAL);
    (* This subprocedure makes one side of the bar. *)
VAR
  count : CARDINAL;
BEGIN
  WriteChar (" ");
  FOR count := 1 TO howWide-1
    DO
      WriteChar ("-");
    END;  (* if *)
END MakeSide;
  
PROCEDURE MakeEnds (howWide : CARDINAL);
VAR
  count : CARDINAL;
BEGIN
  WriteChar ("|");
  FOR count := 2 TO howWide
    DO
      WriteChar (" ");
    END;  (* for *)
  WriteChar ("|");
 END MakeEnds;

BEGIN (* Main DrawBar  procedure *)
  MakeSide (wide);
  WriteLn;
  MakeEnds (wide);
  WriteChar (desc);
  WriteLn;
  MakeSide (wide);
  WriteLn;
END DrawBar;
  
PROCEDURE GetInfo (VAR size : CARDINAL; VAR desc : CHAR);

VAR
  done : BOOLEAN;
BEGIN
  REPEAT   (* for Range checking *)
    ReadCard (size);
    done := (ReadResult () = allRight) AND (size <= 30);
    IF NOT done
      THEN (* bad read or bad info *)
        WriteLn;
        WriteString (" Invalid width input for a bar graph");
      END;   (* if beyond range for graph size *)
    SkipLine;
  UNTIL ReadResult () = allRight;
  WriteString ("Please type in a one character description ==> ");
  ReadChar (desc);
  SkipLine;
  WriteLn;
END GetInfo;

CONST
  numItems = 3;
TYPE
  Bar = 
    RECORD
      width : CARDINAL;
      info : CHAR;
    END;
VAR
  bars : ARRAY [1..numItems] OF Bar;
  count : CARDINAL;

BEGIN   (* Main Program is a simple test of procedures *)
  FOR count := 1 TO numItems
    DO
      WITH bars[count]
        DO
          WriteString ("How many units wide is bar # ");
          WriteCard (count, 1);
          WriteString (" ==> ");
          GetInfo (width, info);
        END;
    END;
   FOR count := 1 TO numItems
    DO
      WITH bars[count]
        DO
          DrawBar (width, info);
        END;
    END;
END BarGraph.

Here is a sample run from this program with the user inputs in bold type .

How many units wide is bar # 1 ==> 25
Please type in a one character description ==> a

How many units wide is bar # 2 ==> 12
Please type in a one character description ==> b

How many units wide is bar # 3 ==> 27
Please type in a one character description ==> c

 ------------------------
|                        |a
 ------------------------
 -----------
|           |b
 -----------
 --------------------------
|                          |c
 --------------------------

Contents