4.4 Predicates

To this point, little regard has been paid to the documentation of procedures. Comments have been included where appropriate, but nothing has been said about any special need to document the role and function of procedures. This needs attention, for procedures are not just portions of a program, or miniature programs on their own, but are pieces of re-usable code that may be called upon under a variety of circumstances. It is important therefore, that each procedure be clearly documented as to:

Such documentation will allow the programmer to avoid errors caused by using a poorly remembered piece of code for some purpose for which it was never intended; the proper use will be outlined in comments that remain with the code. Consider, for instance, a procedure designed to compute what percentage one number is of another:

  PROCEDURE Percent (num, denom : REAL; VAR result : REAL);

  BEGIN
    result := 100.0 * num / denom;
  END Percent;

What will happen if this code is called with denom equalling zero? There are two ways to avoid the divide-by-zero run-time error lurking in indiscriminate use of this procedure. The first is to be up-front about the limitations of the procedure by documenting them:

  PROCEDURE Percent (num, denom : REAL; VAR result : REAL);
  (* calculates what percentage num is of denom *)
  (* pre : denom # 0.0
     post: result = 100.0 * num / denom *)

  BEGIN
    result := 100.0 * num / denom;
  END Percent;

If the procedure is written this way, it becomes the responsibility of the portion of code invoking it to check that the actual parameter passed to Percent is nonzero before going ahead with the procedure.

On the other hand, the responsibility for avoiding a run time error could be given to the procedure itself. If this is done, the main program must be informed when the result is not valid. The procedure could be written:

  PROCEDURE Percent (num, denom : REAL;
				VAR result : REAL; VAR ok : BOOLEAN);
  (* calculates what percentage num is of denom *)
  (* pre : none
     post: if denom # 0.0
		then result = 100.0 * num / denom and ok = true
		else result is undefined and ok is false *)

  BEGIN
    IF denom # 0.0 
      THEN
        result := 100.0 * num / denom;
        ok := TRUE
      ELSE
        ok := FALSE
      END
  END Percent;
Preconditions and postconditions specified in the documentation of a procedure are called predicates.

Example:

Write a program module that can examine two lines, each determined from the coordinates of two points entered in from the keyboard, and determine whether the lines are parallel, perpendicular, or neither.

Discussion:

When two points are available in coordinate form P1(x1, y1) and P2(x2, y2), the slope of the line through the two points is given by



Two lines are parallel if they have the same slope, and they are perpendicular if their slopes are negative reciprocals. A horizontal line has zero slope, and the slope of a vertical line is undefined (it has no slope.) Thus, the failure of a procedure Slope to compute a result is not a bad thing; it just indicates that the line in question is vertical.

MODULE Slopes;

(* Written by R.J. Sutcliffe *)
(* using P1 MPW Modula-2 for the Macintosh computer *)
(* last revision 1991 02 26 *)

FROM STextIO IMPORT
  WriteString, WriteLn, ReadChar, SkipLine; 
FROM SRealIO IMPORT
  ReadReal, WriteFixed;
FROM SIOResult IMPORT
  ReadResult, ReadResults;

VAR
  line1X1, line1Y1, line1X2, line1Y2,
  line2X1, line2Y1, line2X2, line2Y2,
  line1Slope, line2Slope: REAL;
  line1Ok, line2Ok : BOOLEAN;
  key : CHAR;

PROCEDURE GetNum (VAR theNum : REAL);
(* gets a number input from the keyboard
Pre: none
Post: if ok is true then return
        else ask for input to be re-entered *)

VAR
  ok : BOOLEAN;

BEGIN
  REPEAT
    WriteString ("Type the number here ==> ");
    ReadReal (theNum);
    ok := (ReadResult() = allRight);
    SkipLine;
    IF NOT ok (* use saved value for testing *)
      THEN
        WriteLn;
        WriteString ("error in input number; try again.");
        WriteLn;
      END;
  UNTIL ok;
END GetNum;

PROCEDURE Slope (x1, y1, x2, y2: REAL;
			VAR m: REAL; VAR ok: BOOLEAN);
(* computes the slope of a line joining (x1, y1) and (x2, y2)
Pre: none
Post: if x1#x2 then m is the slope and ok is true
        else m is set to the maximum possible real; ok = false *)

VAR
  deltaX, deltaY : REAL;

BEGIN
  deltaX := x2 - x1;
  deltaY := y2 - y1;
  IF deltaX # 0.0
    THEN
      m := deltaY/deltaX;
      ok := TRUE;
    ELSE
      m := MAX (REAL);
      ok := FALSE;
    END;

END Slope;

BEGIN    (* main program *)
  WriteString ("This program computes the slopes of 2 lines");
  WriteString (" from their coordinates.");
  WriteLn;
  WriteLn;
  (* obtain the coordinates *)
  WriteString ("First line:");
  WriteLn;
  WriteString ("First point:");
  WriteString ("x = ");
  GetNum (line1X1);
  WriteString ("y = ");
  GetNum (line1Y1);
  WriteString ("Second point:");
  WriteString ("x = ");
  GetNum (line1X2);
  WriteString ("y = ");
  GetNum (line1Y2);
  WriteString ("Second line:");
  WriteLn;
  WriteString ("First point:");
  WriteString ("x = ");
  GetNum (line2X1);
  WriteString ("y = ");
  GetNum (line2Y1);
  WriteString ("Second point:");
  WriteString ("x = ");
  GetNum (line2X2);
  WriteString ("y = ");
  GetNum (line2Y2);

  (* compute the slopes *)
  Slope (line1X1, line1Y1, line1X2, line1Y2, line1Slope, line1Ok);
  Slope (line2X1, line2Y1, line2X2, line2Y2, line2Slope, line2Ok);

  (* now output the data. *)
  WriteString ("The slope of the first line is ");
  IF line1Ok
    THEN   
      WriteFixed (line1Slope, 6, 0)
    ELSE
      WriteString ("undefined.")
    END;
  WriteLn;
  WriteString ("The slope of the second line is ");
  IF line2Ok
    THEN   
      WriteFixed (line2Slope, 6, 0)
    ELSE
      WriteString ("undefined.")
    END;
  WriteLn;
  WriteString ("The 2 lines are ");
  IF NOT (line1Ok OR line2Ok) OR (line1Slope = line2Slope) 
    THEN
      WriteString ("parallel.");
    ELSIF((line1Slope = 0.0) AND NOT line2Ok) OR
         ((line2Slope = 0.0) AND NOT line1Ok) OR
         (ABS (line1Slope  + (1.0 / line2Slope)) < 0.000001) THEN
      WriteString ("perpendicular.");
    ELSE
      WriteString ("neither parallel nor perpendicular.")
    END;

  WriteLn;
  WriteString ("Press a key to continue ==>");
  ReadChar (key);
  
END Slopes.

NOTES: 1. MAX is a new standard identifier. It returns the maximum value of a type. This has not been done in this case to simulate a value of "infinity," but so that a line with no slope has some value attached to it for purposes of comparison.

2. Observe the comparison (ABS (line1Slope + (1.0 / line2Slope)) < 0.000001). It would be easier to write (line1Slope = - (1.0 / line2Slope)). However, this is unlikely to work in practice because of rounding off effects, so one must instead rely on the two values being close.

3. Observe also the order of checking for perpendicularity. If either of the first two conditions is true, the third will not be checked--a good thing if it could produce a divide-by-zero error.

Here is a single run from this module:

This program computes the slopes of 2 lines from their coordinates.

First line:
First point:x =  please type here ===> 1.0
y =  please type here ===> 3.0
Second point:x =  please type here ===> 3.0
y =  please type here ===> 8.0
Second line:
First point:x =  please type here ===> 2.0
y =  please type here ===> 4.0
Second point:x =  please type here ===> 7.0
y =  please type here ===> 2.0
The slope of the first line is 2.500000
The slope of the second line is -0.4000000
The 2 lines are perpendicular.
Press a key to continue ==>n

Contents