This reading is also available in PDF.
Summary: We consider the constraints we might place upon procedures and mechanisms for expressing those constraints.
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
from the third
reading on recursion
requires its argument to be a
non-empty list of spots. If some careless programmer invokes
spot-list.leftmost and gives it, as an argument, the empty list,
or a list in which one of the elements is not a spot, or perhaps
even some Scheme value that is not a list at all, the computation that the
spot-list.leftmost describes cannot be completed.
> (spot-list.leftmost null) cdr: expects argument of type <pair>; given () > (spot-list.leftmost (list 1)) car: expects argument of type <pair>; given () > (spot-list.leftmost 2) cdr: expects argument of type <pair>; given 2
As you can see, none of these error messages are particularly helpful. Whose responsibility is it to handle these types of errors? As we will see, it is possible to share responsibility between the person who writes a procedure and the person who calls a procedure.
A procedure definition is like a contract between the author of the
definition and someone who invokes the procedure. The
postconditions of the procedure are what the author guarantees:
When the computation directed by the procedure is finished, the
postconditions shall be met. Usually the postconditions are constraints
on the value of the result returned by the procedure. For instance, the
postcondition of the
(define square (lambda (val) (* val val)))
is that the result is the square of the argument
The preconditions are the guarantees that the invoker of a procedure makes
to the author, the constraints that the arguments shall meet. For
instance, it is a precondition of the
square procedure that
val is a number.
If the invoker of a procedure violates its preconditions, then the contract
is broken and the author's guarantee of the postconditions is void. (If
val is, say, a list or a spot, then the author can't very
well guarantee to return its square.) To make it less likely that an
invoker violates a precondition by mistake, it is usual to document
preconditions carefully and to include occasional checks in one's programs,
ensuring that the preconditions are met before starting a complicated
Many of Scheme's primitive procedures have such preconditions, which they enforce by aborting the computation and displaying a diagnostic message when the preconditions are not met:
> (/ 1 0) /: division by zero > (log 0) log: undefined for 0 > (length 116) length: expects argument of type <proper list> given 116
To enable us to enforce preconditions in the same way, most
implementations of Scheme provides a procedure named
which takes a string as its argument. Calling the
procedure aborts the entire computation of which the call is a part and
causes the string to be displayed as a diagnostic message.
For instance, we could enforce
precondition that its parameter be a non-empty list of spots by
rewriting its definition thus:
(define spot-list.leftmost (lambda (spots) (if (or (not (list? spots)) (null? spots) (not (all-spots? spots))) (throw "spot-list.leftmost: requires a non-empty list of spots") (if (null? (cdr spots)) (car spots) (spot.leftmost (car spots) (spot-list.leftmost (cdr spots)))))))
all-spots? is a predicate that we have to write that
takes any list as its argument and determines whether or not all of the
elements of that list are spots.
spot-list.leftmost procedure enforces its precondition:
> (spot-list.leftmost 139) spot-list.leftmost: requires a non-empty list of spots > (spot-list.leftmost null) spot-list.leftmost: requires a non-empty list of spots > (spot-list.leftmost (list 11)) spot-list.leftmost: requires a non-empty list of spots
Including precondition testing in your procedures often makes them markedly easier to analyze and check, 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
although it is useful to test the precondition when the procedure is
from outside by a potentially 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
spot-list.leftmost with two separate procedures, 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
(define spot-list.leftmost (lambda (spots) ; Make sure that spots is a non-empty list of spots (if (or (not (list? spots)) (null? spots) (not (all-spots? spots))) (throw "spot-list.leftmost: requires a non-empty list of spots") ; Find the leftmost of that list. (spot-list.leftmost-kernel spots)))) (define spot-list.leftmost-kernel (lambda (spots) (if (null? (cdr spots)) (car spots) (spot.leftmost (car spots) (spot-list.leftmost (cdr spots))))))
The kernel has the same preconditions as the husk procedure, but does not need to enforce them, because we invoke it only in situations where we already know that the preconditions are satisfied.
The one weakness in this idea is that some potentially irresponsible caller might still call the kernel procedure directly, bypassing the husk procedure that he's supposed to invoke. In later labs, we'll see that there are a few ways to put the kernel back inside the husk without losing the efficiency gained by dividing the labor in this way.
I usually create these pages
on the fly, which means that I rarely
proofread them and they may contain bad grammar and incorrect details.
It also means that I tend to update them regularly (see the history for
more details). Feel free to contact me with any suggestions for changes.
This document was generated by
Siteweaver on Mon Dec 3 09:54:12 2007.
The source to the document was last modified on Fri Oct 12 08:47:54 2007.
This document may be found at
You may wish to validate this document's HTML ; ;Samuel A. Rebelsky, email@example.com
http://creativecommons.org/licenses/by-nc/2.5/or send a letter to Creative Commons, 543 Howard Street, 5th Floor, San Francisco, California, 94105, USA.