In section
, where we began our discussion
of models of evaluation, we noted that Scheme is an
applicative-order language, namely, that all the arguments to Scheme
procedures are evaluated when the procedure is applied. In
contrast, normal-order languages delay evaluation of procedure arguments
until the actual argument values are needed.
Delaying evaluation of procedure arguments until the
last possible moment (e.g., until they are required by a primitive
operation) is called
lazy evaluation.
Consider the procedure
(define (try a b) (if (= a 0) 1 b))Evaluating (try 0 (/ 1 0)) generates an error in Scheme. With lazy evaluation, there would be no error. Evaluating the expression would return 1, because the argument (/ 1 0) would never be evaluated.
An example that exploits lazy evaluation is the definition of a procedure unless
(define (unless condition usual-value exceptional-value) (if condition exceptional-value usual-value))that can be used in expressions such as
(unless (= b 0)
(/ a b)
(begin (display "exception: returning 0")
0))
This won't work in an applicative-order language because both the
usual value and the exceptional value will be evaluated before
unless is called (compare exercise
). An
advantage of lazy evaluation is that some procedures, such as
unless, can do useful computation even if evaluation of some of their
arguments would produce errors or would not terminate.
If the body of a procedure is entered before an argument has been
evaluated we say that the procedure is
non-strict in that
argument. If the argument is evaluated before the body of the
procedure is entered we say that the procedure is
strict in that
argument.
In a purely applicative-order language, all procedures are strict in
each argument. In a purely normal-order language, all compound
procedures are non-strict in each argument, and primitive procedures may be
either strict or non-strict. There are also languages (see
exercise
) that give programmers
detailed control over the strictness of the procedures they define.
A striking example of a procedure that can usefully be made non-strict
is cons (or, in general, almost any constructor for data
structures). One can do useful computation, combining elements to
form data structures and operating on the resulting data structures,
even if the values of the elements are not known. It makes perfect
sense, for instance, to compute the length of a list without knowing
the values of the individual elements in the list. We will exploit
this idea in section
to implement the
streams of chapter 3 as lists formed of non-strict cons
pairs.
Exercise. Suppose that (in ordinary applicative-order Scheme) we define unless as shown above and then define factorial in terms of unless as
(define (factorial n)
(unless (= n 1)
(* n (factorial (- n 1)))
1))
What happens if we attempt to evaluate (factorial 5)? Will our
definitions work in a normal-order language?
Exercise. Ben Bitdiddle and Alyssa P. Hacker disagree over the importance of lazy evaluation for implementing things such as unless. Ben points out that it's possible to implement unless in applicative order as a special form. Alyssa counters that, if one did that, unless would be merely syntax, not a procedure that could be used in conjunction with higher-order procedures. Fill in the details on both sides of the argument. Show how to implement unless as a derived expression (like cond or let), and give an example of a situation where it might be useful to have unless available as a procedure, rather than as a special form.