4.5 Function Procedures

Some of the built-in procedures that have been used thus far can be employed in such a way that they return a value directly to the part of the expression that references them. For instance, programs in this text have contained things like:

  ck := CAP (ch);
  num1 := ABS (num2);
  firstReal := FLOAT (someInteger) + secondReal;
  m := MAX (REAL);

and so on. (More of these will be covered in later chapters.) These have been referred to these all along as functions or, more correctly, as function procedures, but it should be clear that they work somewhat differently than do the regular procedures considered so far in this chapter, because they are oriented toward the return of a single result rather than to the taking of action. Moreover, that result is returned (in effect) in the procedure's own name, rather than in a variable parameter, as any result sent back by a regular procedure must be. The identifier of the procedure is itself being used in the expression as though it were a variable. Note the following definition:

A function procedure is a program unit that optionally accepts input data in its parameter list, but that always assigns an output value to the function name according to a fixed rule or formula determined by the statements in the procedure.

The usual mathematical notation for a function is y = f (x), where the f is the name of the function. In Modula-2 one would write y := f (x) to assign to y the results of calling the function procedure called f with the data x. This time the black box looks a little different, as shown in figure 4.6:

As has already been suggested, Modula-2 employs a variant of the PROCEDURE statement to implement functions. Here is the general form for a function procedure:

PROCEDURE name ( < formal parameter list > ) : <result type>;
<local declarations>;
BEGIN
  <statement sequence>;
  RETURN value;   (* must be somewhere in body *)
END name;

or, diagrammatically, as in figure 4.7:

There are four differences between function procedures and regular procedures:

First: the type of result to be returned must be specified by placing it after a colon following the closing parenthesis of the formal parameter list.

Second: a specific RETURN with the result (of the correct type) that is to be sent back must actually be executed somewhere in the body of the function procedure.

Third: the call of a function procedure must always include a parameter list, even if the latter is empty.

Fourth: the call of a function procedure appears in an expression, not as a separate command on a line by itself as does the call of a regular procedure.

These details are illustrated with some examples. First, here are some valid declarations:

  PROCEDURE Largest (firstReal, secondReal : REAL) : REAL;
  PROCEDURE Strange (VAR firstReal : REAL,
				  secondReal : REAL) : CARDINAL; (* poor code *)
  PROCEDURE Round (realToRound : REAL) : CARDINAL;
  PROCEDURE GetResult ( ) : CARDINAL;
  PROCEDURE CAP (ch : CHAR) : CHAR;

The last of these is just the (implied) heading of one of the built-in pervasive function procedures. The second one does seem a little strange, because it returns both a variable parameter and a result type. In actual practice, it is not likely that anyone will do this very often--indeed, this practice may be legal, but it is very poor code. However, it does illustrate that function procedures are a specialization of regular procedures and share all the capabilities of the latter. To illustrate both this and the third one, here are two ways to write the code for a procedure that rounds off a REAL to the nearest whole number.

 PROCEDURE Round (num : REAL; VAR numOk : BOOLEAN) : CARDINAL;
  BEGIN  (*poor version *)
    IF (num <= FLOAT (MAX (CARDINAL))) AND 
		  (num >= FLOAT (MIN (CARDINAL)))
      THEN
        numOk := TRUE;
        RETURN TRUNC (num + 0.5);
      ELSE
        numOk := FALSE;
        RETURN 0;  (* got to send something back *)
      END;  (* if *)
  END Round;

To use this version, one would write code like:

  answer := Round (numToRound, success);
  IF NOT success
    THEN
    <take evasive action>
  END;

where, numToRound is REAL and success is a BOOLEAN included to ensure that all has gone well. This code is not safe, however, as the (possibly invalid) assignment to answer is made before checking to see if the procedure was successful. This is why function procedures should not be written with variable parameters. It is better to include preconditions in the procedure that are the responsibility of the main code to check before calling the function procedure:

  PROCEDURE Round (num : REAL) : CARDINAL;

  (* This procedure rounds off num to the nearest cardinal.
  Pre: (num <= FLOAT (MAX (CARDINAL))) AND
		(num >= FLOAT (MIN (CARDINAL)))
  Post: the cardinal returned is the closest cardinal to the real parameter supplied *)

  BEGIN
    RETURN TRUNC (num + 0.5);
  END Round;

To use this version, one would write code like:

  IF (numToRound <= FLOAT (MAX (CARDINAL)))
         AND (numToRound >= FLOAT (MIN (CARDINAL)))
    THEN
      answer := Round (numToRound);
    ELSE
      <take evasive action>
    END;

NOTES: 1. There can be as many RETURN statements as necessary. If the logic of the procedure allows any one of them to be encountered, the expression following the RETURN is evaluated and that value assigned to the procedure identifier. Control is immediately transferred to the END of the procedure.

2. If no RETURN <value> has been encountered before the END of the function procedure, a run-time error will be generated.

3. The standard function MIN, like MAX (discussed previously) returns the smallest value for whatever type is given as the parameter.

4. If one writes a function procedure with no parameters, it must be declared and referred to in any calls as Nameofprocedure (), that is, with an empty parameter list.

Problem:

Write a program that will reduce common fractions to lowest terms. The numerator and denominator should be input from the keyboard, and the result printed on the screen.

Discussion:

A fraction is reduced to lowest terms by dividing the numerator and denominator be the GCD of the two. In the last chapter, an algorithm was developed for computing the GCD, and this can become a procedure within the program to be presented here. The GCD program in turn contained a code to swap two numbers, and this too can be abstracted as a single entity and made a separate procedure.

In the following refinement therefore, the step of computing the GCD is not written out in detail but is conceptualized as a single step. However, there are some interesting possibilities that can be encountered when printing out the result, and these are detailed.

Refinement:

Ask the user for the numerator and denominator.
  Read them into cardinal variables.
  set a goodData flag if both are non-zero
Compute and print the result as appropriate:
  if the data was good then
    compute the GCD of the two (use a procedure for this)
    divide the numerator and denominator by the GCD
    print the numerator
    if the denominator is not equal to one
      print a bar for the fraction
      print the denominator
  if the numerator was zero
    then, provided the denominator is not zero, just print a zero
    otherwise, say that the fraction is indeterminate
  if the denominator alone was zero
    say that the fraction is undefined
exit the program
MODULE ReduceFraction;
(* This program reduces fractions to lowest terms. *)

FROM STextIO IMPORT
  WriteString, WriteLn, ReadChar, SkipLine;
FROM SWholeIO IMPORT
  ReadCard, WriteCard;

PROCEDURE Swap (VAR x, y : CARDINAL);
(* swaps two numbers
Pre: none
Post: x(post) = y (pre) and y(post) = x(pre)  *)

VAR
  temp : CARDINAL;

BEGIN  
  temp := x;
  x := y;
  y := temp;
END Swap;

PROCEDURE Gcd (x, y : CARDINAL) : CARDINAL;
(* This procedure computes the greatest common divisor
Pre: the numbers supplied are non-zero
Post: The GCD of the supplied arguments is returned *)

VAR
  result : CARDINAL;

BEGIN
  (* arrange the numbers in order *)
  IF y > x
    THEN 
      Swap (x, y)
    END;
  
  (* compute the GCD *)
  REPEAT
    result := y;  (* let the gcd be the smaller of the two *)
    y := x MOD y; (* replace the smallest with new remainder *)
    x := result; (* & larger with smaller one from last step *)
  UNTIL y = 0;

  RETURN result; (* and send it back *)
  
END Gcd;

VAR (* main program variables *)
  num, denom, gcd : CARDINAL;
  goodData, again : BOOLEAN;
  answer : CHAR;

BEGIN
  WriteString ("This program reduces fractions to lowest terms.");
  WriteLn;

  REPEAT (* main repeat loop *)
    (* get the numbers *)
    WriteString ("Enter the numerator ==> ");
    ReadCard (num);
    SkipLine;
    WriteLn;
    WriteString ("And now, the denominator ==> ");
    ReadCard (denom);
    SkipLine;
    WriteLn;
    
    (* set a flag if the numbers weren't zero *)
    goodData := (num # 0) AND (denom #0);
    
    (* write the result *)
    WriteString ("The lowest terms fraction is");
    WriteLn;
    IF goodData
      THEN
        gcd := Gcd (num, denom);
           (* call the procedure to do the calculation *)
        num := num DIV gcd; (* compute new fraction *)
        denom := denom DIV gcd;
        WriteCard (num, 0);
        WriteLn;
        IF denom # 1  (* denominators of one are not printed *)
          THEN
            WriteString ("-----");
            WriteLn;
            WriteCard (denom, 0);
            WriteLn;
          END;
      ELSIF num = 0 THEN
        IF denom = 0
          THEN
            WriteString ("indeterminate; both parts are zero");
          ELSE (* only numerator is zero, so ok *)
            WriteCard (num, 0);
          END;
      ELSE (* only denominator was zero *)
        WriteString ("undefined because the denominator is zero")
      END;
    WriteLn;
    WriteString ("Do another? Y/N ");
    ReadChar (answer);
    SkipLine;
    again := (CAP (answer) = "Y");
  UNTIL NOT again;
  
END ReduceFraction. 

Here is a run from this program module:

This program reduces fractions to lowest terms.
Enter the numerator ==> 51
And now, the denominator ==> 17
The lowest terms fraction is
3

Do another? Y/N Y
Enter the numerator ==> 91
And now, the denominator ==> 1001
The lowest terms fraction is
1
-----
11

Do another? Y/N Y
Enter the numerator ==> 12
And now, the denominator ==> 0
The lowest terms fraction is
undefined because the denominator is zero
Do another? Y/N Y
Enter the numerator ==> 0
And now, the denominator ==> 0
The lowest terms fraction is
indeterminate; numerator and denominator both zero
Do another? Y/N N

A wide variety of simple functions can be implemented in typical Modula-2 programs. Here is one that squares the supplied real parameter.

  PROCEDURE Sqr (numToSquare : REAL) : REAL;
  (* Pre: numToSquare <= sqrt (MAX (REAL))
  Post: the square of numToSquare is returned *)

  BEGIN
    RETURN numToSquare * numToSquare;
  END Sqr;

Here is one based on a built-in procedure (ODD) that has not previously been mentioned:

  PROCEDURE Even (num : CARDINAL) : BOOLEAN;
  (* Pre : none
  Post : returns true if num is even and false if num is odd *)

  BEGIN
    RETURN NOT ODD (num);
  END;

A little more work is required for some others:

Problem:

Write a procedure to raise a real number to a real exponent.

Discussion:

What is desired here is the solutions to equations such as x = ab. This is done as follows:

Take natural logarithms on both sides, obtaining

Now, raise both sides to the power e (the base of the natural logarithms)

This is expressed in Modula-2 by the assignment

where both exp and ln must be imported from the library module RealMath. This produces the following code:

  PROCEDURE Power (base, exponent : REAL) : REAL;
  (* pre: base raised to the exponent is not greater than MAX (REAL) and base > 0
     post: base raised to the exponent is returned *)

  BEGIN
    RETURN exp (exponent * ln (base))
  END Power;

This function is actually provided as part of RealMath, but in a similar manner, it might be necessary to compute logarithms with some other base than e. (Non ISO versions may provide base ten, or common logarithms in a library). If one starts with

and wishes to find x; one can rewrite changing to exponent notation

and taking natural logarithms on both sides, yields

It is now fairly easy to write:

  PROCEDURE LogBaseB (base, number : REAL) : REAL;
  (* Pre : both base and number are positive reals
  Post : the log of number with the base base is returned. *)

  BEGIN
    RETURN (ln (number)) / (ln (base))
  END LogBaseB;

The mathematics library mentioned above also contains a procedure for computing square roots, but this computation is not difficult, and is demonstrated below:

Problem:

To write a function to compute square roots of real numbers.

Solution:

1. Check to see if the number provided is negative.
2. If it is, return without going further.
3. Otherwise, apply a square root algorithm.
4. Return the answer.

Refinement of 3: (The divide and average method)

Basis: start with a guess at the square root; call it g. Now if g2 = x exactly, then g= x/g and the calculation is finished. If g2 > x then x/g < g so take the average of x/g and g as the next guess and try the test again. When the result is close enough, return it.

3.1  Decide how close the result must be, say epsilon < .00001.
3.2  Guess an initial value. (say, the first guess is set to half the number.)
3.3  While guess and number/guess are not within epsilon of each other
          Take the average of guess and number/guess for a new guess.

The code is formulated in such a way that if the precondition that requires a non-negative parameter is ignored by the client code invoking this routine, it will still be safe. The value returned will be invalid, however.

  PROCEDURE Sqrt (num : REAL) : REAL;

  (* precondition: num >= 0
      postcondition: if num < 0 the answer is invalid,
                     otherwise the square root is returned.
      method: divide and average *)

  CONST
    epsilon = 0.00001;  (* error limit *)

  VAR
    guess : REAL;

  BEGIN
    IF num <= 0.0
      THEN
        RETURN num;
      ELSE
        guess := num/2.0;
      END;
    WHILE ABS  (num/guess - guess ) > epsilon
      DO
        guess :=  0.5 * (num/guess + guess )
      END;
    RETURN guess;

  END Sqrt;

NOTE: The precision of the result could be improved by making the type of the variable LONGREAL and then making epsilon much smaller. Such decisions depend on the range of the two real types in the implementation being used.


Contents