Our environment model of evaluation and our metacircular evaluator execute declarations in sequence, extending the environment frame one declaration at a time. This is particularly convenient for interactive program development, in which the programmer needs to freely mix the application of functions with the declaration of new functions. However, if we think carefully about the internal declarations used to implement block structure (introduced in section 1.1.8), we will find that name-by-name extension of the environment may not be the best way to declare local names.
Consider a function with internal declarations, such as
function f(x) { function is_even(n) { return n === 0 ? true : is_odd(n - 1); } function is_odd(n) { return n === 0 ? false : is_even(n - 1); } // rest of body of f }
Our intention here is that the name is_odd in the body of the function is_even should refer to the function is_odd that is declared after is_even. The scope of the name is_odd is the entire body of f, not just the portion of the body of f starting at the point where the declaration of is_odd occurs. Indeed, when we consider that is_odd is itself defined in terms of is_even—so that is_even and is_odd are mutually recursive functions—we see that the only satisfactory interpretation of the two declarations is to regard them as if the names is_even and is_odd were being added to the environment simultaneously. More generally, in block structure, the scope of a local name is the entire function body in which the declaration is evaluated.
As it happens, our interpreter will evaluate calls to
f
correctly, but for an accidental
reason:
Since the
declarations
of the internal
functions
come first, no calls to these
functions
will be evaluated until all of them have been
declared.
Hence,
is_odd
will have been
declared
by the time
is_even
is executed. In fact, our sequential evaluation mechanism will give the
same result as a mechanism that directly implements simultaneous
declaration
for any
function
in which the
internal
declarations
come first in a body and
evaluation of the value expressions for the
declared names
doesn't
actually use any of the
declared names.
(For an example of a
function
that doesn't obey these restrictions,
so that sequential
declaration
isn't equivalent to
simultaneous
declaration,
see exercise 4.11.)[1]
There is, however, a simple way to treat
declarations
so that
internally
declared
names have truly simultaneous scope—just create
all local
names
that will be in the current environment before
evaluating any of the value expressions.
One way to do this is by a
syntax transformation on
function definition expressions.[2]
Before evaluating the body of a
function definition expression,
we
scan out
and eliminate all the internal
declarations
in the body. The internally
declared names
will be created with a
function definition
and then set to their
values by assignment.
In the following, we shall focus on
variable declarations using
let; constant declarations
using const and
function can be handled
similarly.
For example, the
function definition
($\textit{vars}$) => {
let u = $e_1$;
let v = $e_2$;
$\textit{statement}$
}
would be transformed into
( $\textit{vars}$ ) => {
return ( (u, v) => {
u = $e_1$;
v = $e_2$;
$\textit{statement}$
})("*unassigned*", "*unassigned*");
}
where
"*unassigned*"
is a special symbol
that causes looking up a
name
to signal an error if an attempt is made to use the
value of the not-yet-assigned
name.
An alternative strategy for scanning out internal declarations is shown in exercise 4.10. Unlike the transformation shown above, this enforces the restriction that the declared names' values can be evaluated without using any of the names' values.
simultaneousscope rule for internal declarations without constructing the extra frame.
function solve(f, y0, dt) { const y = integral( () => dy, y0, dt); const dy = stream_map(f, y); return y; }
let a = 1; function f(x) { let b = a + x; let a = 5; return a + b; } f(10);
(n => (fact => fact(fact, n)) ( (ft, k) => k === 1 ? 1 : k * ft(ft, k - 1) ) ) (10);
function f(x) { function is_even(n) { return n === 0 ? true : is_odd(n - 1); } function is_odd(n) { return n === 0 ? false : is_even(n - 1); } return is_even(x); }
function f(x) { return ( (is_even, is_odd) => is_even(is_even, is_odd, x) ) ( (ev, od, n) => n === 0 ? true : od(??, ??, ??), (ev, od, n) => n === 0 ? false : ev(??, ??, ??) ); }
management is not responsibleremark in footnote 1 of chapter 1. The designers of JavaScript chose to resolve this issue by moving all internal function declarations to the beginning of the function body, and thus the discussion might seem moot. However, this mechanism is only applied to function declarations and not to const declarations.
pure $\lambda$-calculusimplementation of recursion. (See