To illustrate what we mean by having a computational object with
time-varying state, let us model the situation of withdrawing money
from a
bank account. We will do this using a
procedurefunctionwithdraw, which takes as argument an
amount to be withdrawn.
If there is enough money in the account to accommodate the withdrawal,
then withdraw should return the balance
remaining after the withdrawal. Otherwise,
withdraw should return the message
Insufficient funds. For example, if we begin with $100
in the account, we should obtain the following sequence of responses
using
withdraw:
Original
Python
(withdraw 25)
75
print(withdraw(25))
75
Original
Python
(withdraw 25)
50
print(withdraw(25))
50
Original
Python
(withdraw 60)
"Insufficient funds"
print(withdraw(60))
"Insufficient funds"
Original
Python
(withdraw 15)
35
print(withdraw(15))
35
Observe that the expression
(withdraw 25),withdraw(25),
evaluated twice, yields different values. This is a new kind of
behavior for a
procedure.function.
Until now, all our
proceduresPython functions
could be viewed as specifications for computing mathematical functions.
A call to a
procedurefunction
computed the value of the function applied to the given arguments,
and two calls to the same
procedurefunction
with the same arguments always produced the same
result.[1]
To implement withdraw, we can use a
variable balance to indicate the balance of
money in the account and define withdraw
as a
procedurefunction
that accesses balance.
The withdrawprocedurefunction
checks to see if balance is at least as large
as the requested amount. If so,
withdraw decrements
balance by amount
and returns the new value of balance. Otherwise,
withdraw returns the Insufficient funds
message. Here are the
definitionsdeclarations
of balance and
withdraw:
Decrementing balance is accomplished by the
expressionassignment
Original
Python
(set! balance (- balance amount))
balance = balance - amount
Original
Python
This uses the set! special form, whose
syntax is
(set! $\langle \textit{name} \rangle$ $\langle \textit{new-value}\rangle$)
Here
$\langle \textit{name} \rangle$
is a symbol and
$\langle \textit{new-value} \rangle$
is any expression.
This statement looks like a declaration assignment, but it does
not declare a new variable. The global declaration
global balance
ensures that the name
balance is already
declared in the body of the function
withdraw
and refers to the globally declared variable
balance.
An assignment of the form
$name$ = $new$-$value$
where the $name$ is already declared, is called
a reassignment.
Set!The reassignment
changes
$\langle \textit{name} \rangle$
$name$
so that its value is the
result obtained by evaluating
$\langle \textit{new-value}\rangle$.
$new$-$value$.
In the case at hand, we are changing balance so
that its new value will be the result of subtracting
amount from the previous value of
balance.[2]
Original
Python
Withdraw also uses the
begin
special form to cause two expressions to be evaluated in the case where
the if test is true: first decrementing
balance and then returning the value of
balance. In general, evaluating the
expression
(begin $\textit{exp}_{1}$ $\textit{exp}_{2}$ $\ldots$ $\textit{exp}_{k}$)
causes the expressions $\textit{exp}_{1}$
through $\textit{exp}_{k}$ to be evaluated in
sequence and the value of the final expression
$\textit{exp}_{k}$ to be returned as the
value of the entire begin
form.[3]
The function withdraw also uses a
sequence of statements to cause two statements to be evaluated
in the case where the if test is
true: first decrementing balance
and then returning the value of
balance.
In general, executing a sequence
$stmt$$_{1}$ $stmt$$_{2} \ldots$$stmt$$_{n}$
causes the statements $stmt$$_{1}$
through
$stmt$$_{n}$ to be evaluated in
sequence.[4]
Although withdraw works as desired, the
variable balance presents a problem. As
specified above, balance is a name defined
in the
globalprogram
environment and is freely accessible to be examined or
modified by any
procedure.function.
It would be much better if we could somehow make
balance internal to
withdraw, so that
withdraw would be the only
procedurefunction
that could access balance directly and
any other
procedurefunction
could access balance only indirectly
(through calls to withdraw). This would
more accurately model the notion that
balance is a local state variable used by
withdraw to keep track of the state of the
account.
We can make balance internal to
withdraw by rewriting the definition as
follows:
What we have done here is use let to
establish an environment with a local variable
balance, bound to the initial value 100.
Within this local environment, we use
lambda to create a procedure that takes
amount as an argument and behaves like our
previous withdraw procedure. This
procedure—returned as the result of evaluating the
let expression—is
new-withdraw,
which behaves in precisely the same way as
withdraw but whose variable
balance is not accessible by any other
procedure.[5]
What we have done here is use a declaration assignment
to establish an environment with a local variable
balance, bound to the initial
value 100. Within this local environment, we use a function
declaration to
create a function
withdraw
that takes amount
as an argument and—returned as the result of evaluating the body of the
make_withdraw_balance_100
function—behaves in precisely the same way as
our previous
withdraw
function,
but its variable
balance is not accessible by any
other function.[6]
Original
Python
The nested withdraw
function faces a similar problem as the previous
global withdraw function:
The assignment
balance = balance - amount
should refer to the variable balance declared outside
of the withdraw function,
and should not redeclare the name
balance. However, now the name
balance is not a global name, but a name that is declared
in the surrounding function
make_withdraw_balance_100.
Python uses nonlocal declarations in this situation.
The declaration
nonlocal balance
in the nested withdraw function indicates that any assignment
to the name balance inside the withdraw function refers
to the variable balance declared outside the function
but not globally.[7]
Combining
set!
with local variables
reassignments with declaration assignments
is the general programming
technique we will use for constructing computational objects with
local state. Unfortunately, using this technique raises a serious
problem: When we first introduced
procedures,functions,
we also introduced the substitution model of evaluation
(section 1.1.5) to provide an
interpretation of what
procedurefunction
application means. We said that applying a
procedurefunction whose body is a return statement
should be interpreted as evaluating the
body of the procedurereturn expression of the function
with the
formal
parameters replaced by their values.
For functions with more complex
bodies, we need to evaluate the whole body with the
parameters replaced by their values.
The trouble is that,
as soon as we introduce assignment into our language, substitution is no
longer an adequate model of
procedurefunction
application. (We will see why this is so in
section 3.1.3.) As a consequence, we
technically have at this point no way to understand why the
new-withdrawnew_withdrawprocedurefunction
behaves as claimed above. In order to really understand a
procedurefunction
such as
new-withdraw,new_withdraw,
we will need to develop a new model of
procedurefunction
application. In section 3.2 we will
introduce such a model, together with an explanation of
set! and local variables.reassignment statements and declaration assignments.
First, however, we examine some variations on the theme established by
new-withdraw.new_withdraw.
The following
procedure, make-withdraw,
function, make_withdraw,
creates withdrawal processors.
The formal parameter
balance in
make-withdrawmake_withdraw
specifies the initial amount of money in the
account.[8]
Observe that W1 and
W2 are completely independent objects, each
with its own local state variable balance.
Withdrawals from one do not affect the other.
We can also create objects that handle
deposits as well as
withdrawals, and thus we can represent simple bank accounts. Here is
a
procedurefunction
that returns a bank-account object with a specified initial
balance:
Each call to make_account sets up an
environment with a local state variable balance.
Within this environment, make_account defines
proceduresfunctionsdeposit and
withdraw that access
balance and an additional
procedurefunctiondispatch
that takes a message as input and returns one of the two local
procedures.functions.
The dispatchprocedurefunction
itself is returned as the value that represents the bank-account object.
This is precisely the
message-passing style of programming that we saw in
section 2.4.3, although here we are using
it in conjunction with the ability to modify local variables.
Make-account
The function
make_account
can be used as follows:
Original
Python
(define acc (make-account 100))
acc = make_account(100)
Original
Python
((acc 'withdraw) 50)
50
print(acc("withdraw")(50))
50
Original
Python
((acc 'withdraw) 60)
"Insufficient funds"
print(acc("withdraw")(60))
"Insufficient funds"
Original
Python
((acc 'deposit) 40)
90
print(acc("deposit")(40))
90
Original
Python
((acc 'withdraw) 60)
30
print(acc("withdraw")(60))
30
Each call to acc returns the locally defined
deposit or withdrawprocedure,function,
which is then applied to the specified amount.
As was the case with
make-withdraw, another
call to make-accountmake_withdraw, another
call to make_account
Original
Python
(define acc2 (make-account 100))
acc2 = make_account(100)
will produce a completely separate account object, which maintains its
own local balance.
Exercise 3.1
An
accumulator is a
procedurefunction
that is called repeatedly with a single numeric argument and accumulates its
arguments into a sum. Each time it is called, it returns the currently
accumulated sum. Write a
procedurefunctionmake-accumulatormake_accumulator
that generates accumulators, each maintaining an independent sum. The
input to
make-accumulatormake_accumulator
should specify the initial value of the sum; for example
Original
Python
(define A (make-accumulator 5))
a = make_accumulator(5)
Original
Python
(A 10)
15
print(a(10))
15
Original
Python
(A 10)
25
print(a(10))
25
def make_accumulator(current):
def add(arg):
nonlocal current
current = current + arg
return current
return add
Exercise 3.2
In software-testing applications, it is useful to be able to count the
number of times a given
procedurefunction
is called during the course of a computation. Write a
procedurefunctionmake-monitoredmake_monitored
that takes as input a
procedure,function,f, that itself takes one input. The result
returned by
make-monitoredmake_monitored
is a third
procedure,function,
say mf, that keeps track of the number of times
it has been called by maintaining an internal counter. If the input to
mf is the
special symbol how-many-calls,
string "how many calls",
then mf returns the value of the counter. If
the input is the
special symbol reset-count,string "reset count",
then mf resets the counter to zero. For any
other input, mf returns the result of calling
f on that input and increments the counter.
For instance, we could make a monitored version of the
sqrtprocedure:function:
Original
Python
(define s (make-monitored sqrt))
s = make_monitored(math_sqrt)
Original
Python
(s 100)
10
print(s(100))
10
Original
Python
(s 'how-many-calls?)
1
print(s("how many calls"))
1
s = make_monitored(math_sqrt)
s(100)
print(s("how many calls"))
s(5)
print(s("how many calls"))
def make_monitored(f):
counter = 0 # initialized to 0
def mf(cmd):
nonlocal counter
if cmd == "how many calls":
return counter
elif cmd == "reset count":
counter = 0
return counter
else:
counter = counter + 1
return f(cmd)
return mf
Exercise 3.3
Modify the
make-accountmake_accountprocedurefunction
so that it creates
password-protected accounts. That is,
make-accountmake_account
should take a
symbolstring
as an additional argument, as in
Original
Python
(define acc (make-account 100 'secret-password))
acc = make_account(100, "secret password")
The resulting account object should process a request only if it is
accompanied by the password with which the account was created, and
should otherwise return a complaint:
print(acc("some other password", "deposit")(40))
"Incorrect password"
def make_account(balance, p):
def withdraw(amount):
nonlocal balance
if balance >= amount:
balance = balance - amount
return balance
else:
return "Insufficient funds"
def deposit(amount):
nonlocal balance
balance = balance + amount
return balance
def dispatch(m, q):
if p == q:
return (withdraw if m == "withdraw"
else deposit if m == "deposit"
else "Unknown request: make_account")
else:
return lambda q: "Incorrect Password"
return dispatch
a = make_account(100, "eva")
a("withdraw", "eva")(50) # withdraws 50
print(a("withdraw", "ben")(40)) # incorrect password
Exercise 3.4
Modify the
make-accountmake_accountprocedurefunction
of exercise 3.3 by adding another
local state variable so that, if an account is accessed more than seven
consecutive times with an incorrect password, it invokes the
procedurefunctioncall-the-cops.call_the_cops.
def call_the_cops(reason):
return "calling the cops because " + reason
def make_account(balance, p):
invalid_attempts = 0 # initializes to 0
def withdraw(amount):
nonlocal balance
if balance >= amount:
balance = balance - amount
return balance
else:
return "Insufficient funds"
def deposit(amount):
nonlocal balance
balance = balance + amount
return balance
def calling_the_cops(_):
return call_the_cops("you have exceeded " +
"the max no of failed attempts")
def dispatch(m, q):
nonlocal invalid_attempts
if invalid_attempts > 7:
return calling_the_cops
elif p == q:
invalid_attempts = 0 # reset after a correct password
return (withdraw if m == "withdraw"
else deposit if m == "deposit"
else "Unknown request: make_account")
else:
invalid_attempts = invalid_attempts + 1
if invalid_attempts > 7:
return calling_the_cops
else:
return lambda x: "Incorrect Password"
return dispatch
[1]
Actually, this is not quite true. One exception was the
random-number generator
in section 1.2.6. Another exception
involved the
operation/type tables we introduced in
section 2.4.3, where the values of two
calls to get with the same arguments
depended on intervening calls to put.
On the other hand, until we introduce
assignment,reassignment,
we have no way to
create such
proceduresfunctions
ourselves.
[2]
The value of a set! expression is
implementation-dependent. Set! should be
used only for its effect, not for its value.
The name set! reflects a naming convention
used in Scheme: Operations that change the values of variables (or that
change data structures, as we will see in
section 3.3) are given names that end
with an exclamation point. This is similar to the convention of
designating predicates by names that end with a question mark.
Reassignment statements and declaration assignments
look similar to and should not be confused with
expressions of the form
$expression_1$ == $expression_2$
which evaluate to True
if $expression$$_1$ evaluates to the
same value as $expression$$_2$ and to
False otherwise.
[3]
We have already used
cond and in
begin implicitly in our programs, because in
Scheme the body of a procedure can be a sequence of expressions. Also,
the consequent part of each clause in a
cond expression can be a sequence of
expressions rather than a single expression.
[4]
We have already used
sequences implicitly in our programs, because in
Python the body block
of a function can contain a sequence of function definitions
followed by a return statement, not
just a single return statement,
as discussed in
section 1.1.8.
[5]
In programming-language jargon, the variable
balance is said to be
encapsulated within the
new-withdraw
procedure. Encapsulation reflects the general system-design principle
known as the
hiding principle: One can make a system more modular and robust
by protecting parts of the system from each other; that is, by providing
information access only to those parts of the system that have a
need to know.
[6]
In programming-language jargon, the variable
balance is said to be
encapsulated within the
new_withdraw function.
Encapsulation reflects the general system-design principle known as the
hiding principle: One can
make a system more modular and robust by protecting parts of the
system from each other; that is, by providing information access only
to those parts of the system that have a need to
know.
[7]
Similar to global declarations,
a nonlocal declaration does not extend to functions that
are nested inside the function in which the nonlocal declaration
appears. For these nested functions to reassign the
nonlocal variable, they also need to declare the variable as nonlocal.
[8]
In contrast with
new-withdrawmake_withdraw_balance_100
above, we do not have to use
let
a declaration assignment
to make balance a local variable, since
formal
parameters are already
local. This will be clearer after the discussion of the environment
model of evaluation in
section 3.2.
(See also
exercise 3.10.)