must eventually restore it to its previous value. For example, a procedure to print in octal might look like:
(DEFINE (PRINT-8 N)
((LAMBDA (OLDRADIX)
(PROGN (SETQ RADIX 8)
(PRINT-NUMBER N)
(SETQ RADIX OLDRADIX)))
RADIX))
This convention allows PRINT-8 to locally alter the radix, in a manner transparent to its caller; it does not interfere with the way in which its caller may be using PRINT.
This convention is a standard pattern of use. It is a stack discipline on the values of RADIX (or whatever other variables). We would like to capture this pattern as an abstraction in our language.
Surprise! We have seen this abstraction before: dynamically scoped variables behave in precisely this way. Dynamically scoped variables conceptually have a built-in side effect — we took advantage of this at the end of Part One to fix the problem with the top-level loop. Binding a dynamically scoped variable such as RADIX can be said to cause a side effect because it alters the behavior of a (superficially) unrelated procedure such as PRINT in a referentially opaque manner. Such binding is a particularly structured kind of side effect, because it guarantees that the side effect will be properly undone when the binder has finished executing. Thus with dynamic scoping we could write:
(DEFINE (PRINT-8 N)
((LAMBDA (RADIX)
(PRINT-NUMBER N))
8))
We saw in Part One that, precisely because dynamically scoped variables are referentially opaque, we do not want all variables to be dynamically scoped. But we have newly rediscovered dynamic variables in another context and found them desirable. We therefore consider an interpreter which supplies both lexical and dynamic variables (see Figure 14).
Here we have merged the dynamically scoped variable evaluator (Figure 5) with the lexically scoped evaluator (Figure 11). We changed APPLY to have an extra case, wherein an "open LAMBDA-expression" is effectively closed at the time of its application using the environment of its caller. EVAL is changed to once again supply the environment to APPLY. This interpreter is almost identical to that of LISP 1.5 [LISP 1.5M], with, the difference that we write simply (LAMBDA ...)
to get a closed procedure where in LISP 1.5 one must write (FUNCTION (LAMBDA ...))
; in both cases one must write '(LAMBDA ...)
to get an open LAMBDA
-expression.