We can turn to the environment model to see how
proceduresfunctions
and
assignmentreassignment
can be used to represent objects with local state. As an
example, consider the
withdrawal processor from
section 3.1.1 created by calling the
procedurefunction
Figure 3.15
Figure 3.16
shows the result of
defining the
make-withdrawdefining the
make_withdrawprocedurefunction
in the global environment. This produces a
procedurefunction
object that contains a pointer to the
global
environment. So far, this is no different from the examples we have already
seen, except that
the body of the
procedure is itself a
lambda
expression.
the function withdraw defined in the body
of make_withdraw is returned by
make_withdraw.
Original
Python
Figure 3.15
Result of defining make-withdraw in the
global environment.
Figure 3.16
Result of defining
make_withdraw in
the global environment.
The interesting part of the computation happens when we apply the
procedurefunctionmake-withdrawmake_withdraw
to an argument:
Original
Python
(define W1 (make-withdraw 100))
W1 = make_withdraw(100)
Original
Python
We begin, as usual, by setting up an environment E1 in which the
formal parameter
balance is bound to the argument 100.
Within this environment, we evaluate the body of
make-withdraw,
namely the
lambda expression. This
constructs a new procedure object, whose code is as
specified by the
lambda
and whose environment is E1, the environment in which the
lambda
was evaluated to produce the
procedure.
We begin by setting up an environment E1 in which the
parameter
balance is bound to the argument 100
and the locally declared variable
withdraw is unassigned.
Within this environment, we evaluate the body of
make_withdraw,
namely a function definition and a return statement.
The function definition constructs a function object
which is bound to withdraw in E1.
The
resulting procedurefunction
object is the value returned by the call to
make-withdraw.make_withdraw.
This is bound to W1 in the
global environment, since the
definedeclaration assignment
itself is being evaluated in the global environment.
Figure 3.17
Figure 3.18
shows the resulting environment structure.
Original
Python
Figure 3.17
Result of evaluating
(define W1 (make-withdraw 100)).
Figure 3.18
Result of evaluating
W1 = make_withdraw(100).
Now we can analyze what happens when W1
is applied to an argument:
Original
Python
(W1 50)
50
W1(50)
We begin by constructing a frame in which
amount, the
formal parameter of
W1, is bound to the argument 50.
Because withdraw declares
balance as
nonlocal,
balance is not declared locally in
withdraw, and the new frame contains no
binding for balance.
The crucial
point to observe is that this frame has as its enclosing environment not the
global environment, but rather the environment E1, because this is the
environment that is specified by the W1procedurefunction
object. Within this new environment, we evaluate the body of the
procedure:function:
The resulting environment structure is shown in
figure 3.19.
figure 3.20.
The expression being evaluated references
both amount and
balance.
AmountThe variable amount
will be found in the first frame in the environment, and
balance will be found by following the
enclosing-environment pointer to E1.
Original
Python
Figure 3.19
Environments created by applying the procedure object
W1.
Figure 3.20
Environments created by applying the function object
W1.
When the
set!reassignment
is executed, the binding of balance in E1 is
changed. At the completion of the call to
W1, balance is 50,
and the frame that contains balance is still
pointed to by the
procedurefunction
object W1. The frame that binds
amount (in which we executed the code that
changed balance) is no longer relevant, since
the
procedurefunction
call that constructed it has terminated, and there are no pointers to that
frame from other parts of the environment. The next time
W1 is called, this will build a new frame that
binds amount and whose enclosing environment is
E1. We see that E1 serves as the place that holds the local
state variable for the
procedurefunction
object W1.
Figure 3.21
Figure 3.22
shows the situation after the call to W1.
Observe what happens when we create a second withdraw object
by making another call to
make_withdraw:make_withdraw:
Original
Python
(define W2 (make-withdraw 100))
W2 = make_withdraw(100)
This produces the environment structure of
figure 3.23,
figure 3.24,
which shows
that W2 is a
procedurefunction
object, that is, a pair with some code and an environment. The environment
E2 for W2 was created by the call to
make-withdraw.make_withdraw.
It contains a frame with its own local binding for
balance. On the other hand,
W1 and W2 have the
same code: the code specified by the
lambda expressionfunction definition of withdraw
in the body of
make-withdrawmake_withdraw.[1] We see
here why W1 and W2
behave as independent objects. Calls to
W1 reference the state variable
balance stored in E1, whereas calls to
W2 reference the
balance stored in E2. Thus, changes to the
local state of one object do not affect the other object.
Original
Python
Figure 3.23
Using (define W2 (make-withdraw 100)) to
create a second object.
Figure 3.24
Using
W2 = make_withdraw(100)
to create a second object.
Exercise 3.10
In the
make-withdrawmake_withdrawprocedure,function
the local variable balance is created as a
parameter of
make-withdraw.make_withdraw.
We could also create the local state variable
explicitly,
separately,
using
let,
what we might call an
immediately invoked function
definition
as follows:
Recall from section 1.3.2 that
let is simply syntactic sugar for a
procedurefunction
call:
(let ((var exp)) body)
is interpreted as an alternate syntax for
((lambda (var) body) exp)
The init function
is invoked immediately after it
is defined. Its only purpose is to create a local variable
balance and
initialize it to initial_amount.
Use the environment model to analyze this alternate version of
make_withdraw, drawing figures like the ones
above to illustrate the interactions
Show that the two versions of
make-withdrawmake_withdraw
create objects with the same behavior. How do the environment structures
differ for the two versions?
Black shows the environment structure of function in exercise 3.10
Green shows differences in environment structure of original
(where make_withdraw is replaced with the figure 3.9 version)
Exercise 3.11
In section 3.2.4, we
saw how internal
function definitions
definitions
work.
Now we have seen how the
environment model described the behavior of
proceduresfunctions
with local state.
A typical message-passing
procedurefunction
contains both of these aspects. Consider the
bank account
procedurefunction
of section 3.1.1:
Show the environment structure generated by the sequence of
interactions
Original
Python
(define acc (make-account 50))
acc = make_account(50)
Original
Python
((acc 'deposit) 40)
90
print(acc('deposit')(40))
90
Original
Python
((acc 'withdraw) 60)
30
print(acc('withdraw')(60))
30
Where is the local state for acc kept?
Suppose we define another account
Original
Python
(define acc2 (make-account 100))
acc2 = make_account(100)
How are the local states for the two accounts kept distinct? Which parts
of the environment structure are shared between
acc and acc2?
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.
[1]
Whether
W1 and W2 share
the same physical code stored in the computer, or whether they each keep a
copy of the code, is a detail of the implementation. For the interpreter we
implement in chapter , the code is in fact shared.