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 or Dr. 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)
;; Make sure that LS is a non-empty list of strings.
(if (or (null? ls)
(not (list-of-strings? ls)))
(error 'longest-on-list
"the argument must be a non-empty list of strings"))
;; Find the longest string on the list.
(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. The Dr. Scheme messages are as follows:
> (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.
In Dr. Scheme, similar messages are produced, and execution stops when the error is encountered.
Review your monomial procedure from the lab on (more) recursion
(exercise 4f). Note that the computation will yield a
meaningful result only if the value of the exponent is a non-negative
integer. Add a precondition test to your procedure to ensure that the
argument is an integer but not negative.
Expand your error checking by adding further precondition that both the coefficient and x-value are numbers.
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 unnecessary 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)
;; Make sure that LS is a non-empty list of strings.
(if (or (null? ls)
(not (list-of-strings? ls)))
(error 'longest-on-list
"the argument must be a non-empty list of strings"))
;; Find the longest string on the list.
(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.
Write a husk-and-kernel version of the dupl procedure
(exercise 5 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.
In Dr. Scheme, the same parameters may be used for the error,
although it also is acceptable to include just the diagnostic message --
omitting the procedure name.
When the error procedure is invoked, Chez Scheme interrupts
that computation in progress, stores it so that it can be examined later by
special-purpose software tools (such as the Chez Scheme inspector,
which allows you to study the structure of an interrupted computation), 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 another procedure that can be used like
error and takes 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.)
Replace the call to error in the longest-on-list
procedure above with a call to warning; run the resulting
procedures on arguments that don't meet the preconditions and observe what
happens.
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.)
Hints: To print a warning and return a 0, you need to group the warning expression and the return value into a single unit. This may be done in at least two ways:
Write a separate procedure process-warning which does both actions. Since a procedure will return the value of the final expression as the value of a procedure, you can put several expressions in a procedure, and all but the last expression will be side-effects of evaluating that procedure.
In this case, the process-warning
procedure can be called by the husk procedure sum-of-list to
process the null-list case, just as another procedure can be called by the
husk procedure to determine the sum of a non-null list.
Combine both actions in an and statement, as discussed at the start
of the lab on Conditional Execution in
Scheme. (The and evaluates successive arguments until all are
processed or until
Define a predicate irr-list? that takes one argument and
determines whether that argument is a list containing exactly three
arguments, the first an integer and the second and third real numbers.
(The predicate should return #t if all of these
conditions are met, #f if any one of them is not
satisfied.)
This document is available on the World Wide Web as
http://www.math.grin.edu/~walker/courses/151.sp99/lab-detecting-errors.html
created February 12, 1997 by John David Stone
last revised September 17, 1999 by Henry M. Walker