10.13 An Extended Example--Fractions and Exceptions

The purpose of this section is to illustrate some of the ideas presented in the last section in a longer piece of code. For simplicity, easy comparison, and to avoid having to present planning steps not relevant to this section, consider one way of adding appropriate exceptions to the Module Fractions first developed in chapter six.

Three operations with fractions can be identified as ones that, if proper data is provided, there will be no error, but if improper data is passed the fractions are invalid. (This makes the module a good candidate for having some exceptions defined--it can control its own code, but not the mistaken calls of clients using erroneous data.) These are:

1. assignment of a zero denominator,

2. taking the inverse when there is a zero numerator, and

3. dividing when the divisor has zero numerator.

These are similar errors, and all could be given the same exception identifier, but for the purpose of illustrating features of exceptions, all three will be treated separately.

In the definition module that follows, alterations have been made to the original in accordance with the remarks in the last section.

DEFINITION MODULE Fractions;

(* Written by R.J. Sutcliffe *)
(* using ISO Modula-2 *)
(* to illustrate exceptions use in libraries *)
(* last revision 1994 05 31 *)

TYPE
  Fraction = ARRAY [1 .. 2] OF INTEGER;
  (* the first component is the numerator; the second the denominator *)

PROCEDURE Assign (m, n : INTEGER) : Fraction;
  (* If n is not equal to zero, then the fraction returned has m as numerator and n as denominator. Otherwise the exception zeroDenominator is raised. *)

PROCEDURE Numerator (x : Fraction) : INTEGER;
  (* the numerator of the fraction is returned *)

PROCEDURE Denominator (x : Fraction) : INTEGER;
  (* the denominator of the fraction is returned *)

PROCEDURE Neg (x : Fraction) : Fraction;
  (* Pre: the fraction returned has the numerator negated *)

PROCEDURE Inv (x : Fraction) : Fraction;
  (* If the numerator of x is not equal to zero then the fraction returned has numerator and denominator swapped.  Otherwise the exception noInverse is raised. *)

PROCEDURE Add (x, y : Fraction) : Fraction;
  (* The fraction returned is the sum x plus y *)

PROCEDURE Sub (x, y : Fraction) : Fraction;
  (* The fraction returned is the difference x minus y *)

PROCEDURE Mul (x, y : Fraction) : Fraction;
  (* The fraction returned is the product of x and y *)

PROCEDURE Div (x, y : Fraction) : Fraction;
  (* If the numerator of y is not equal to 0, then the fraction returned is the quotient of x by y.  Otherwise, the exception zeroDivide is raised *)

TYPE
  FracExceptions = (zeroDenominator, noInverse, zeroDivide);

PROCEDURE IsFracException (): BOOLEAN;
  (* Returns TRUE if the current coroutine is in the exceptional execution state because of the raising of an exception from FracExceptions; otherwise returns FALSE. *)

PROCEDURE FracException (): FracExceptions;
  (* If the current coroutine is in the exceptional execution state because of the raising of an exception from FracExceptions, returns the corresponding enumeration value, and otherwise raises an exception. *)

END Fractions.

Note in the following implementation that a choice has been made to report any exception occurrence and print the associated string in the termination part of the module.

IMPLEMENTATION MODULE Fractions;

(* Written by R.J. Sutcliffe *)
(* using ISO Modula-2 *)
(* to illustrate exceptions use in libraries *)
(* last revision 1994 05 31 *)

FROM EXCEPTIONS IMPORT
  ExceptionSource, AllocateSource, RAISE, IsExceptionalExecution, IsCurrentSource, CurrentNumber, GetMessage;
FROM STextIO IMPORT
  WriteString, WriteLn, SkipLine, ReadChar;

VAR
  fracExSource : ExceptionSource;

PROCEDURE Assign (m, n : INTEGER) : Fraction;

VAR
  temp : Fraction;

BEGIN
  IF n = 0
    THEN
      RAISE (fracExSource, ORD (zeroDenominator), "Cannot assign fraction with zero denominator");
    ELSE
      temp [1] := m;
      temp [2] := n;
      RETURN temp;
    END;
END Assign;

PROCEDURE Numerator (x : Fraction) : INTEGER;

BEGIN
  RETURN x [1];
END Numerator;

PROCEDURE Denominator (x : Fraction) : INTEGER;

BEGIN
  RETURN x [2];
END Denominator;

PROCEDURE Neg (x : Fraction) : Fraction;

BEGIN
  x [1] := -x [1];
  RETURN x;
END Neg;

PROCEDURE Inv (x : Fraction) : Fraction;

VAR
  temp : INTEGER;

BEGIN;
  IF Numerator (x) = 0
    THEN
      RAISE (fracExSource, ORD (noInverse), "Cannot invert fraction with zero numerator");
    ELSE
      temp := x [1];
      x [1] := x [2];
      x [2] := temp;
      RETURN x;
    END;
END Inv;

PROCEDURE Add (x, y : Fraction) : Fraction;

VAR
  temp : Fraction;

BEGIN
  temp [1] := x [1] * y [2] + x [2] * y [1];
  temp [2] := x [2] * y [2];
  RETURN temp;
END Add;

PROCEDURE Sub (x, y : Fraction) : Fraction;

BEGIN
  RETURN Add (x, Neg (y) );
END Sub;

PROCEDURE Mul (x, y : Fraction) : Fraction;

BEGIN
  RETURN Assign (x [1] * y [1], x [2] * y [2]);
END Mul;

PROCEDURE Div (x, y : Fraction) : Fraction;

BEGIN
  IF Numerator (y) = 0
    THEN
      RAISE (fracExSource, ORD (zeroDivide), "Cannot divide by zero");
    ELSE
      RETURN Mul (x, Inv (y) );
    END;
END Div;

PROCEDURE IsFracException (): BOOLEAN;
BEGIN
   RETURN (IsExceptionalExecution() )  AND (IsCurrentSource (fracExSource) )
END IsFracException;


PROCEDURE FracException (): FracExceptions;
(* The call to CurrentNumber will raise ExException automatically if this source didn't raise an exception. *)
BEGIN
  RETURN VAL (FracExceptions, CurrentNumber (fracExSource) );
END FracException;


VAR
  errorMessage : ARRAY [0..255] OF CHAR;
  
BEGIN (* initialize *)
   AllocateSource (fracExSource);
   
FINALLY
  IF IsFracException ()
    THEN
      GetMessage (errorMessage);
      WriteString ("Program terminating because of exception");
      WriteLn ;
      WriteString (errorMessage);
      WriteLn;
      WriteString ("Type return to continue");
      SkipLine;
    END;
        
END Fractions.

There are slight dangers in putting code into a FINALLY clause that depends on the importation of some other module, as in this case.

First, if termination is caused by an exception during initialization, then it may be that the module required (STextIO here) has not yet been initialized (and therefore cannot be used correctly). This should not happen in this case, because all the exceptions that can be raised by this module are in code that is unlikely to be called during the initialization of library modules.

Second, modules are terminated in the reverse order that they are initialized. If the module being employed in FINALLY clause has already been terminated, it may be that some facilities it offers are no longer available. (An implementation may choose to close all channels during finalization, for instance). This latter problem can only be determined to exist by examining the implementation documentation.

Note that if either problem does exist, there is no work around, because:

The correctness of a program depends on the meaning of its individual modules, but any program whose meaning depends on the order of initialization (or finalization) of its modules is incorrect.

In this case, the following simple application was run to do a limited test of the new exceptions, by forcing one of them to be raised:

MODULE TestFractions;

IMPORT Fractions;

VAR
  p, q, r : Fractions.Fraction;
  
BEGIN
  p := Fractions.Assign (0, -3);
  q := Fractions.Assign (4, -3);
  r := Fractions.Div (q, p);
END TestFractions.

The exception was triggered, and checking the output log after the run was complete revealed its contents as:

** Run log starts here **
Program terminating because of exception
Cannot divide by zero
Type return to continue

Contents