5.4 The FOR Statement

A number of the examples thus far in the text have employed counting loops of the following general construction:

  count := start;
  WHILE count <= stop
    DO
      statement sequence;
      INC (count);
    END;

Such loops are likely to be common when dealing with indexed data, and in particular for arrays. They may be used for counting through the indices of an array, for counting the number of times to perform a computation, or both. So common are such tasks that a special name is given to them:

Any task that is repeated according to a numbered pattern is said to be iterative. The act of such counted repetition, whether of numbered items in a list such as an array, or of steps in a program, such as a loop, is called iteration.

In common with most computing notations, Modula-2 has a special shorthand loop syntax for performing iterations. This is called the FOR loop, and can be described with the following skeleton--a direct replacement for the code in the above WHILE loop:

  FOR count := start TO stop
    DO
      statement sequence;
    END;

The initialization of the loop counter variable to its starting value, and the specification of the stopping point are both handled in the first line of the loop. There is no need for the programmer to write a statement for incrementing the loop counter, as this action is included in the FOR programming abstraction itself. When the END of the FOR loop is reached, the loop counter variable is increased by 1 and the new value checked to see if it is beyond the stopping value. If not, the statement sequence under the control of the loop is executed again. If the counter has gone past the stopping value, the loop exits to the next statement following it in the main program sequence.

NOTES: 1. FOR and TO are reserved words.

2. This code is prettyprinted like the IF .. THEN statement, with the END under the DO.

3. The loop control variable (count in this case) must have been previously declared as an ordinal type. It need not have been previously assigned, however, because the FOR statement itself starts with the initial assignment for the sake of starting the loop.

4. Variables or expressions are allowed to control the FOR, but must be of a countable (ordinal) type. That is, CARDINAL, CHAR, and enumerated or subrange types can be used, but REAL is not allowed.

Here are some examples:

Allowed:

  FOR count := start TO index + 3
  (* start, index are expression compatible, and are assignment compatible with count *)
    DO
      Statement Sequence; (* semicolon optional here as usual*)
    END;

  FOR ch = 'A' TO 'Z' (* ch previously declared of type CHAR *)
    DO
      Write (ch);
      WriteLn;
    END;

Not Allowed:

  FOR realNum := 1.0 TO 5.0  (* uses a real *)
  FOR count := 1 to 10.5  (* same problem *)

May cause a problem:

In the discussion that follows 32767 is being suggested as MAX(INTEGER). This low limit is unlikely on current machines; however there will always be a value of which the point made here does become relevant.

  FOR count := 32465 TO 70000 (* outside CARDINAL range on some machines *)
  FOR integerTypeCounter := 1 TO 32767

The latter will not work in some versions, because after count becomes 32767, it would next be 32768, and in many implementations this is past the limit of the INTEGER type. What happens next in such cases depends on the implementation. An overflow like this ought to generate an error at run time, but if it does not, 32768 may instead "wrap around" to MIN (INTEGER) and be interpreted as -32768 when the test for exit from the loop is made. Since this is less than 32767, the loop would continue forever. The same logic applies for other implementations of INTEGER with the number 32767 replaced by MAX (INTEGER).

The logic of an actual program may require that the loop skip numbers, count in reverse, or both. If using the WHILE loop, these were accommodated by using INC (count, increment), DEC (count), or DEC (count, decrement). To do the same thing in a FOR loop, the increment or decrement is specified in the opening line along with the starting and stopping values with the reserved word BY followed by a whole number amount. Thus,

  count := start;
  WHILE count <= stop
    DO
      statement sequence;
      INC (count, increment);
    END;

becomes

  FOR count := start TO stop BY increment
    DO
      statement sequence;
    END;

and likewise,

  count := start;
  WHILE count >= stop
    DO
      statement sequence;
      DEC (count, decrement);
    END;

becomes

  FOR count := start TO stop BY -decrement
    DO
      statement sequence;
    END;

That is, one can think of the FOR loop as being shorthand for these WHILE loops. The loop test is at the top, so the FOR loop will not even be entered if the value of the start expression is already beyond that of the stop expression. The looping action ceases when the control variable goes past the stopping value. Thus the loop

  FOR count := 1 TO 10 BY 2

will execute when count has the values 1, 3, 5, 7, and 9 and will terminate when count has the value 11. Likewise

  FOR count := 10 TO 1 BY -2

will execute when count has the values 10, 8, 6, 4, and 2, but terminates when count has the value 0. On the other hand, the code

  start := 6;
  stop := 13;
  FOR count := start TO stop BY -2

will skip the loop altogether.

It is now possible to provide a syntax diagram (figure 5.2) for this statement.

Notice that if a constant BY step is included, it must be a constant expression of a whole number type.

Correct:

  FOR day := Friday TO Monday BY -1
    DO

Incorrect:

  FOR day := Friday TO Monday BY Thursday (* can't use non-numeric *)
    DO

 FOR day := Friday TO Monday BY integerVariable (* can't use a variable *)
    DO

  FOR count := 1 TO 5 BY 1.5 (* increment must be whole *)
    DO

Care must be taken when using any loop with a variable of a subrange type. Suppose one had:

  TYPE
    Digit = [0..9];
  VAR
   digitCount : Digit;

  BEGIN
    FOR digitCount := 0 TO 9
      DO
        statementSequence;
      END;

On the last pass through this loop, an attempt might be made to set digitCount to 10, a value that is out of range for this type. Consequently, a run time error could be generated. Some compilers may produce correct code (that would not fail) but situations like this may have to be avoided.

5.4.1 The FOR Loop and the WHILE Loop

Like the WHILE loop, the FOR loop has a top-of-loop test. As has been observed above, any FOR loop is equivalent to (and can therefore be replaced by) an equivalent WHILE construction. However, the FOR loop can only count up or down in constant increments, whereas the WHILE construction places no limitations on re-assigning the loop counter variable. Thus,

  count := 100;
  WHILE count > 0
    DO
      statement sequence
      count := count DIV 2
    END;

executes for count holding values of 100, 50, 25, 12, 6, 3, and 1. It concludes when count gets the value zero. Because the loop depends on the values of an ordinal variable, it is a counting loop, in the most general sense, but because the sequence of values it takes are not in an arithmetic sequence with a common spacing, this loop is not, in the sense the term is used in this text, an iteration. (It cannot be replaced by a FOR loop.)

One can also use this technique test for some condition within a loop, and if the condition exists, set the WHILE loop control variable to a value beyond the stopping value and force an exit from the loop before the count would otherwise have reached the stopping value.

  count := start;
  WHILE count > stop
    DO
      statement sequence1;
      IF certainCondition
        THEN
          count := stop - 1
        END;
     INC (count);
    END;

When the test for the stopping condition is made at the top of the loop, no distinction is made between the two ways in which the value of count might have reached stop and be indicating that it is time to exit the loop code.

However, this kind of chicanery is not permitted with a FOR loop. Indeed, reassigning the loop control variable in the code under its control is an error. (It is also quite confusing, because the first line of the FOR statement ought to give reliable information about the number of times the loop will execute.) The Modula-2 rule is:

The loop control variable of a FOR loop may not be threatened by code within the loop.

Thus, the compiler ought to flag as an error any code such as:

  FOR count := 100 TO 0 BY -2 (* error *)
    DO
      statement sequence
      count := count DIV 2
    END;

or

  FOR count := 1 TO 10 
    DO
      count := 2 * count + 1;    (* do not do this either *)
      (* more statements here *)
    END;

The variable can be used, even though it cannot be assigned within the loop, as in the following simple example:

MODULE AlphabetWriter; (* revised 1993 02 26 *)

FROM STextIO IMPORT
  WriteChar, WriteLn;
FROM SWholeIO IMPORT
  WriteCard;

VAR
  ch : CHAR;
CONST
  skip = 2;

BEGIN
  FOR ch := 'A' TO 'Z'
    DO
      WriteChar (ch);  (* writes whole uppercase alphabet *)
    END;
  WriteLn;
  FOR ch := 'z' TO 'a' BY -1
    DO
      WriteChar (ch);  (* writes lowercase letters backwards *)
    END;
  WriteLn;
  FOR ch := 'A' TO 'Z' BY skip
    DO
      WriteCard (ORD (ch), 3);  (* writes every second ordinal *)
    END;
END AlphabetWriter.

This program produced the following when run:

ABCDEFGHIJKLMNOPQRSTUVWXYZ
zyxwvutsrqponmlkjihgfedcba
 65 67 69 71 73 75 77 79 81 83 85 87 89

A compiler that conforms to the ISO standard Modula-2 specification will catch and report as errors any threats to a loop control variable during compilation. Classical standard Modula-2 had the same rule, but very few of its compilers enforced the rule. However, there are other, more subtle, ways to threaten the value of the loop control variable than simple reassignment and even the experts argue about whether the standard covers all the possibilities. Because checking for all these is rather difficult, some sub-standard compilers do not bother with the issue at all, and permit erroneous code to be written. Programmers ought to take note, however, that if they do get away with such a trick, the meaning of the program will then depend on code that is incorrect. The program will not compile properly under a standard conforming compiler unless changes are made.

Some people claim that since a WHILE loop is a more general form of which the FOR loop can be regarded as a special case, there is no need for the latter. As can be seen, there is something to be said for this view. A few teachers go further, and forbid their students to use the FOR construction altogether. This text is sympathetic to that view to a small extent, in that it has postponed the use of the FOR loop to a point at which it is hoped that the student understands the (more general) WHILE construction.

However, the two kinds of loops are not interchangeable, for some WHILE loops cannot be replaced by a FOR loop. Thus, while one could dispense with FOR loops, the two are actually different programming abstractions. The FOR loop has been specialized for incremental counting (iterations), and ought to be used for that purpose, leaving the WHILE and REPEAT constructions for loops involving logical tests, non-incremental counting, and situations where the loop control variable is REAL.

There is yet another difference between the Modula-2 FOR loop and WHILE loop, and indeed between the Modula-2 FOR loop and FOR loops in other computing notations. It is expressed in the following rule:

At the conclusion of a Modula-2 FOR loop, the value of the loop control variable is undefined.

This does not mean that the loop control variable cannot be re-used once the loop has exited. It simply means that it cannot be relied upon to have any particular value, and so it must be re-initialized before being used for anything else.

Correct:

  FOR count := 1 to 10
    DO
      statement sequence1;
    END;  (* first for *)
  FOR count := 1 TO 100
    DO
      statement sequence2;
    END; (* second for *)
  count := 2;
  statement sequence 3; (* one that depends on count being 2 *)

Incorrect:

  FOR count := 1 to 10
    DO
      statement sequence1;
    END;  (* first for *)
  IF count = 11 THEN  (* count is actually undefined here *)
    DO
      statement sequence3;
    END

  FOR count := 1 TO 100
    DO
      statement sequence2;
    END; (* second for *)
  count := count + 2;  (* count is undefined here *)
  statement sequence 3;

The two statement sequences in the second group are not syntactically incorrect; they will compile without any error being reported by the compiler. However, they are logically incorrect, in that they apparently rely on the loop control variable being set to some particular value after the loop is exited, and Modula-2 says nothing about what such a value will be, so different implementations might produce different results. A program whose correctness depends on the way a particular compiler happens to treat such a situation is incorrect. Even this assumes that a given implementation treats this problem consistently, and that is not required either. Very good Modula-2 compilers will even generate a message warning the programmer of such ill-advised usage.

5.4.2 The FOR Loop in Use

To further illustrate some of these ideas, consider the following:

Problem:

A class set of marks is to be entered in the form of a student's name on one line, followed by a line of real percentages separated by spaces. At the beginning of the data entry are two cardinals. The first is the number of student records to be entered, and the second is the number of marks per student. Write a program to read these records one at a time, print the student's name, grades, and average mark, followed by the class average on the composite marks.

Sample Input:

3
4
Janet
87.5 92.8 89.0 90.0
Fred
45.8 81.9 78.0 88.5
Laurie
0.0 15.8 32.5 53.5

Code:

MODULE ClassMarks;
(* Written by R.J. Sutcliffe *)
(* to illustrate the use of the for loop *)
(* using ISO standard Modula-2 *)
(* last revision 1991 02 27 *)

FROM STextIO IMPORT
  WriteLn, WriteString, ReadString, SkipLine, ReadChar, WriteChar;
FROM SWholeIO IMPORT
  ReadCard, WriteCard;
FROM SRealIO IMPORT
  ReadReal, WriteFixed;

TYPE
  NameString = ARRAY [0..10] OF CHAR;

VAR
  theMark, studSum, studMean, classSum, classMean : REAL;
  numMarks, numStudents, markCount, classCount : CARDINAL;
  name : NameString;
  key : CHAR;

BEGIN
  (* initialize variable for sum of marks *)
  classSum := 0.0;
  (* write information *)
  WriteString ("This program opens a set of class records");
  WriteLn;
  WriteString ("and computes student and class averages. ");
  WriteLn;
  WriteLn;
  (* find out how many students *)
  WriteString ("How many students are in the group? ");
  ReadCard (numStudents); 
  SkipLine;
  WriteLn;
  (* find out how many marks *)
  WriteString ("How many marks does each have?");
  ReadCard (numMarks); 
  SkipLine;
  WriteLn;
  (* get class info one student record at a time *)
  FOR classCount := 1 TO numStudents
    DO
      WriteString ("Student Name, Please ==>");
      ReadString (name);
      SkipLine;
      WriteLn;
      studSum := 0.0;
      WriteString ("Please enter the");
      WriteCard (numMarks ,0);
      WriteString (" marks for ");
      WriteString (name);
      WriteString (", separated by spaces on a single line");
      WriteLn;
      WriteString ("==> ");
     FOR markCount := 1 TO numMarks
        DO (* all marks for each student *)
          ReadReal (theMark); (* get a number from the list *)
          studSum := studSum + theMark; (* reset the sum *)
        END; (* for markCount *)
      SkipLine;
      studMean := studSum / FLOAT (numMarks);
      WriteString ("Average = ");
      WriteFixed (studMean, 2, 6);
      WriteLn; (* done one student *)

      classSum := classSum + studMean;
    END; (* for classCount, done whole class when exit here *)

  classMean := classSum / FLOAT (numStudents);
  
  WriteString ("The average of the ");
  WriteCard (numMarks, 0);
  WriteString (" marks for the class of  ");
  WriteCard (numStudents, 0);
  WriteString (" students was ");
  WriteFixed (classMean, 2, 0);
  WriteString (" percent."); 
  WriteLn;
  WriteString ("Press a key to continue ==> ");
  ReadChar (key);

END ClassMarks.

The program was run with the sample data above with the following results:

This program opens a set of class records
and computes student and class averages. 

How many students are in the group? 3

How many marks does each have?4

Student Name, Please ==>Janet

Please enter the 4 marks for Janet, separated by spaces on a single line
==> 87.5 92.8 89.0 90.0
Average =  89.82
Student Name, Please ==>Fred

Please enter the 4 marks for Fred, separated by spaces on a single line
==> 45.8 81.9 78.0 88.5
Average =  73.55
Student Name, Please ==>Laurie

Please enter the 4 marks for Laurie, separated by spaces on a single line
==> 0.0 15.8 32.5 53.5
Average =  25.45
The average of the  4 marks for the class of   3 students was  62.94 percent.
Press a key to continue ==> q

The technique of indicating the number of data items that will be read by the FOR loop at the beginning of the input is a common one, but it does have drawbacks. The data to be read by this code must have exactly the specified structure, and each item must be successfully read for this program to work correctly. The FOR loop construction cannot itself check the value of ReadResult() as an additional reason to exit the loop. Therefore, data of this kind is usually written to a file under the control of a program, and then input to this kind of a program from the file. Such methods may also be used to help check for errors; these can await the introduction of file handling methods in subsequent chapters.

Some people like to write fancy output on the screen with boxes made of stars or carefully calculated spacing. They use the FOR loop in statements that output the same character several times. Assuming an eighty character wide screen, the code

  FOR count := 1 TO 80
    DO
      Write ("*")
    END;

will print a line of stars on the screen. The loops that follow place on the screen ten lines consisting of blanks, except for the first and last characters, which are stars.

  FOR countA := 1 TO 10 (* ten lines in total *)
    DO
      Write ("*");   (* start with star *)

      FOR countB := 2 TO 79   (* then the blanks *)
        DO
          Write (" ")
        END;    (* for countB *)

      Write ("*")   (* end with star *)

     END;    (* for countA *)

Following this with another copy of the first loop would complete an 80 column by 10 row box surrounded with stars.

********************************************************************************
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
*                                                                              *
********************************************************************************

Contents