In this section we handle the evaluation of function bodies or other
blocks (such as the branches of conditional statements) that contain
declarations.
Each block opens a new scope for names declared in the block.
In order to evaluate a block in a given environment, we extend that
environment by a new frame that contains all names declared directly
(that is, outside of nested blocks) in the body of the block and
then evaluate the body in the newly constructed environment.
Section 1.1.8 introduced the idea that
functions
can have internal
declarations,
thus leading to a block structure as in the
following
function
to compute square roots:
function sqrt(x) {
function is_good_enough(guess) {
return abs(square(guess) - x) < 0.001;
}
function improve(guess) {
return average(guess, x / guess);
}
function sqrt_iter(guess){
return is_good_enough(guess)
? guess
: sqrt_iter(improve(guess));
}
return sqrt_iter(1);
}
Now we can use the environment model to see why these internal
declarations
behave as desired.
Figure 3.22
shows the point in the evaluation of the expression
sqrt(2)
where the internal
function
is_good_enough
has been called for the first time with
guess equal to 1.
Observe the structure of the environment.
The name
sqrt is bound
in the program environment
to a
function
object whose associated environment is the
program
environment. When sqrt was called, a new
environment, E1, was formed, subordinate to the
program
environment, in which the parameter x is bound
to 2. The body of sqrt was then
evaluated in E1.
That body is a block with local
function declarations and therefore E1 was extended with a new frame for
those declarations, resulting in the new environment E2. The body
of the block was then evaluated in E2. Since the first statement
in the body is
function is_good_enough(guess) {
return abs(square(guess) - x) < 0.001;
}
evaluating this declaration created the function
is_good_enough
in the environment E2.
To be more precise,
the name is_good_enough
in the first frame of E2 was bound to a function
object whose associated environment is E2.
Similarly,
improve and
sqrt_iter
were defined as
functions in E2.
For conciseness,
figure 3.22
shows only the
function
object for
is_good_enough.
After the local
functions
were defined, the expression
sqrt_iter(1)
was evaluated, still in environment
E2.
So the
function
object bound to
sqrt_iter
in E2 was called with 1 as an argument. This created an environment E3
in which
guess, the parameter of
sqrt_iter,
is bound to 1.
The function sqrt_iter
in turn called
is_good_enough
with the value of guess
(from E3) as the argument for
is_good_enough.
This set up another environment,
E4, in which
guess (the parameter of
is_good_enough)
is bound to 1. Although
sqrt_iter
and
is_good_enough
both have a parameter named guess, these are two
distinct local variables located in different frames.
Also, E3 and E4 both have E2 as their enclosing environment, because the
sqrt_iter
and
is_good_enough functions
both have E2 as their environment part.
One consequence of this is that the
name
x that appears in the body of
is_good_enough
will reference the binding of x that appears in
E1, namely the value of x with which the
original sqrt
function
was called.
The environment model thus explains the two key properties that make local
function declarations
a useful technique for modularizing programs:
The names of the local
functions
do not interfere with
names external to the enclosing
function,
because the local
function
names will be bound in the frame that the
block creates when it is evaluated,
rather than being bound in the
program
environment.
The local
functions
can access the arguments of the enclosing
function,
simply by using parameter names as free
names.
This is
because the body of the local
function
is evaluated in an environment that is subordinate to the
evaluation environment for the enclosing
function.
In section 3.2.3 we saw how the
environment model described the behavior of
functions
with local state. Now we have seen how internal
declarations
work.
A typical message-passing
function
contains both of these aspects. Consider the
bank account
function
of section 3.1.1:
There is currently no solution available for this exercise. This textbook adaptation is a community effort. Do consider contributing by providing a solution for this exercise, using a Pull Request in Github.
More about blocks
ブロックについてもう少し
As we saw, the scope of the names declared in
sqrt is the whole body of
sqrt. This explains why
mutual recursion works, as in this (quite
wasteful) way of checking whether a nonnegative
integer is even.
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);
}
At the time when
is_even is called during a call to
f, the environment diagram looks
like the one in figure 3.22 when
sqrt_iter is called. The functions
is_even and
is_odd are bound in E2 to function objects
that point to E2 as the environment in which to evaluate calls to those
functions. Thus
is_odd in the body of
is_even refers to the right function.
Although
is_odd
is defined after
is_even,
this is no different from how in the body of
sqrt_iter
the name
improve
and the name
sqrt_iter
itself refer to the right functions.
Equipped with a way to handle declarations within blocks, we can
revisit declarations of names at the top level. In
section 3.2.1, we saw
that the names declared at the top level are added to the program
frame. A better explanation is that the whole program is placed in
an implicit block, which is evaluated in the global environment.
The treatment of blocks described above then handles the top
level:
The global environment is extended by a frame that contains the
bindings of all names declared in the implicit block. That frame is
the program frame and the resulting
environment is the
program environment.
We said that a block's body is evaluated in an environment that
contains all names declared directly in the body of the block.
A locally declared name is put into the environment when the block is
entered, but without an associated value. The evaluation of its
declaration during evaluation of the block body then assigns to the
name the result of evaluating the expression to the right of the
=, as if the declaration were
an assignment. Since the addition of the name to the environment is
separate from the evaluation of the declaration, and the whole block
is in the scope of the name, an erroneous program could attempt to
access the value of a name before its declaration is evaluated;
the evaluation of an unassigned name signals an error.
This explains why the program in
footnote undefined of chapter 1 goes wrong.
The time between creating the binding for a name and evaluating
the declaration of the name is called the
temporal dead zone (TDZ).