Detecting errors

Several of the Scheme procedures that we have written or studied in preceding labs presuppose that their arguments will meet specific preconditions -- constraints on the types or values of its arguments. For instance, the longest-on-list procedure from the lab on recursion won't work unless it is given a non-empty list of strings:

> (longest-on-list '(3 6 7))

Error in string-length: 6 is not a string.
Type (debug) to enter the debugger.
> (longest-on-list '())

Error in cdr: () is not a pair.
Type (debug) to enter the debugger.

So far, we've been relying on the underlying implementation of Chez Scheme to detect and report such errors. Sometimes this is an adequate way of dealing with them, but in other cases one might prefer to write the procedure in such a way that it checks and enforces its preconditions before performing any operations on its arguments.

In Chez Scheme, this can be done by adding, at the beginning of the body of the procedure, an if-expression that tests the precondition and invokes a procedure named error if it is not met:

(define longest-on-list
  (lambda (ls)
    (if (or (null? ls)
            (not (list-of-strings? ls)))
        (error 'longest-on-list
               "the argument must be a non-empty list of strings"))
    (if (null? (cdr ls))
        (car ls)
        (longer-string (car ls)
                       (longest-on-list (cdr ls))))))

(define list-of-strings?
  (lambda (ls)
    (or (null? ls)
        (and (pair? ls)
             (string? (car ls))      
             (list-of-strings? (cdr ls))))))

The list-of-strings? procedure tests whether its argument is a list in which each element is a string. (In English: ls is a list of strings if it is either the empty list or a pair in which the first element is a string and the rest is a list of strings.) The definition of the longest-on-list procedure is the same as the one provided in the lab on recursion, except that the precondition is now being tested: The error procedure is invoked if either the incoming argument is empty or it is something other than a list of strings.

With this version of longest-on-list, the error messages are different:

> (longest-on-list '(3 6 7))

Error in longest-on-list: the argument must be a non-empty list of strings.
Type (debug) to enter the debugger.
> (longest-on-list '())

Error in longest-on-list: the argument must be a non-empty list of strings.
Type (debug) to enter the debugger.

The error is detected and reported before the procedure gets down to the point where it might try to apply string-length to a number or cdr to an empty list.

  1. Recover your quadratic-root procedure from the lab on procedure definitions. Note that it can complete its computation only if the value of the first argument is non-zero. Add precondition tests to your procedure to ensure that every argument is a number and that the first argument is not zero.

Including precondition testing in your procedures often makes them markedly easier to analyze and debug, so I recommend the practice, especially during program development. There is a trade-off, however: It takes time to test the preconditions, and that time will be consumed on every invocation of the procedure. Since time is often a scarce resource, it makes sense to save it by skipping the test when you can prove that the precondition will be met. This often happens when you, as programmer, control the context in which the procedure is called as well as the body of the procedure itself.

For example, in the preceding definition of longest-on-list, although it is useful to test the precondition when the procedure is invoked ``from outside'' by an irresponsible caller, it is a waste of time to repeat the test of the precondition for any of the recursive calls to the procedure. At the point of the recursive call, you already know that ls is a list of strings (because you tested that precondition on the way in) and that its cdr is not empty (because the body of the procedure explicitly tests for that condition and does something other than a recursive call if it is met), so the cdr must also be a non-empty list of strings. So it's superfluous to confirm this again at the beginning of the recursive call.

One solution to this problem is to replace the definition of longest-on-list with two separate procedures, a ``husk'' and a ``kernel.'' The husk interacts with the outside world, performs the precondition test, and launches the recursion. The kernel is supposed to be invoked only when the precondition can be proven true; its job is to perform the main work of the original procedure, as efficiently as possible:

(define longest-on-list
  (lambda (ls)
    (if (or (null? ls)
            (not (list-of-strings? ls)))
        (error 'longest-on-list
               "the argument must be a non-empty list of strings"))
        (longest-on-list-kernel ls)))

(define longest-on-list-kernel
  (lambda (ls)
    (if (null? (cdr ls))
        (car ls)
        (longer-string (car ls)
                       (longest-on-list-kernel (cdr ls))))))

In later labs, we'll see that there are a couple of ways to put the kernel back inside the husk without losing the efficiency gained by dividing the labor in this way.

  1. Write a husk-and-kernel version of the replicate procedure (from the first lab on recursion) that checks that its first argument is a non-negative integer before launching the recursion.

The error procedure in Chez Scheme takes two arguments. The first should always be the symbol that names the procedure inside which the error has occurred -- in our example, the symbol longest-on-list. The second should be a diagnostic -- a string that states specifically what precondition failed.

When the error procedure is invoked, Chez Scheme interrupts that computation in progress, stores it where the debugger and inspector can get hold of it, and returns you to the top level with an error message. The symbol and the diagnostic string that were given as arguments to error appear in this message.

Chez Scheme provides two other procedures that can be used like error and take the same two arguments. The warning procedure prints out a warning message, but does not interrupt the computation in progress; instead, Chez Scheme rushes on and attempts to complete the job. (Often it eventually encounters an actual error and gives up, but one can call warning regardless of whether or not it presages an error.) The break procedure prints out an error message and immediately starts the debugger, bypassing the top level.

  1. Replace the call to error in the longest-on-list procedure above first with a call to warning and then with a call to break; run the resulting procedures on arguments that don't meet the preconditions and observe what happens.

  2. Write a procedure sum-of-list that takes any list of numbers and returns the sum of of the elements of the list. Have the procedure print a warning message if it is given an empty list. (The procedure should return 0 after issuing this warning message.)


This document is available on the World Wide Web as

http://www.math.grin.edu/courses/Scheme/detecting-errors.html

created February 12, 1997
last revised May 28, 1997
John David Stone (stone@math.grin.edu)