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
JavaScript
(withdraw 25)
75
withdraw(25);
75
Original
JavaScript
(withdraw 25)
50
withdraw(25);
50
Original
JavaScript
(withdraw 60)
"Insufficient funds"
withdraw(60);
"Insufficient funds"
Original
JavaScript
(withdraw 15)
35
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
proceduresJavaScript 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]
Original
JavaScript
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.
So far, all our names have been immutable.
When a function was applied, the values that its parameters
referred to never changed, and once a declaration was evaluated,
the declared name never changed its value.
To implement functions like
withdraw, we introduce
variable declarations, which use the keyword
let, in addition to constant
declarations, which use the keyword
const.
We can declare a variable
balance
to indicate the balance of money
in the account and define
withdraw as a function 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:
let balance = 100;
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
} else {
return "Insufficient funds";
}
}
Decrementing balance is accomplished by the
expressionexpression statement
Original
JavaScript
(set! balance (- balance amount))
balance = balance - amount;
Original
JavaScript
This uses the set! special form, whose
syntax is
(set! $\langle \textit{name} \rangle$ $\langle \textit{new-value}\rangle$)
The syntax of
assignment expressions is
$name$ = $new$-$value$
Here
$\langle \textit{name} \rangle$
is a symbol
$name$
has been declared with
let or
as a
function parameter
and
$\langle \textit{new-value} \rangle$
$new$-$value$
is any expression.
Set!The assignment
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
JavaScript
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]
In JavaScript, the return value of functions is determined by
return statements, which
can appear in the first statement of a sequence. To make matters
even more complex, at JavaScript top level, the value of a sequence is
the value of the first component, if the second component is not
value-producing. Thus in JavaScript,
1; const x = 2;
evaluates to the value 1. We decide to ignore the subtleties of the
JavaScript top level here. The return value of non-top-level sequences
are determined by the placement of return statements, which we will
explain later.
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 let
to establish an environment with a local variable
balance, bound to the initial
value 100. Within this local environment, we use a lambda
expression[6] to
create a function that takes amount
as an argument and behaves like our previous
withdraw function. This
function—returned as the result of evaluating the body of the
make_withdraw_balance_100
function—behaves in precisely the same way as
withdraw, but its variable
balance is not accessible by any
other function.[7]
Combining
set!
with local variables
assignments with variable declarations
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.assignments and variable declarations.
First, however, we examine some variations on the theme established by
new-withdraw.new_withdraw.
Parameters of functions as well as names declared with
let are
variables.
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:
function make_account(balance) {
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
} else {
return "Insufficient funds";
}
}
function deposit(amount) {
balance = balance + amount;
return balance;
}
function dispatch(m) {
return m === "withdraw"
? withdraw
: m === "deposit"
? deposit
: error(m, "unknown request -- make_account");
}
return dispatch;
}
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
JavaScript
(define acc (make-account 100))
const acc = make_account(100);
Original
JavaScript
((acc 'withdraw) 50)
50
acc("withdraw")(50);
50
Original
JavaScript
((acc 'withdraw) 60)
"Insufficient funds"
acc("withdraw")(60);
"Insufficient funds"
Original
JavaScript
((acc 'deposit) 40)
90
acc("deposit")(40);
90
Original
JavaScript
((acc 'withdraw) 60)
30
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
JavaScript
(define acc2 (make-account 100))
const 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
JavaScript
(define A (make-accumulator 5))
const a = make_accumulator(5);
Original
JavaScript
(A 10)
15
a(10);
15
Original
JavaScript
(A 10)
25
a(10);
25
function make_accumulator(current) {
function add(arg) {
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
JavaScript
(define s (make-monitored sqrt))
const s = make_monitored(math_sqrt);
Original
JavaScript
(s 100)
10
s(100);
10
Original
JavaScript
(s 'how-many-calls?)
1
s("how many calls");
1
const s = make_monitored(math_sqrt);
s(100);
display(s("how many calls"));
s(5);
display(s("how many calls"));
function make_monitored(f) {
let counter = 0; //initialized to 0
function mf(cmd) {
if (cmd === "how many calls") {
return counter;
} else if (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
JavaScript
(define acc (make-account 100 'secret-password))
const 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:
acc("some other password", "deposit")(40);
"Incorrect password"
function make_account(balance, p) {
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
} else {
return "Insufficient funds";
}
}
function deposit(amount) {
balance = balance + amount;
return balance;
}
function dispatch(m, q) {
if (p === q) {
return m === "withdraw"
? withdraw
: m === "deposit"
? deposit
: "Unknown request: make_account";
} else {
return q => "Incorrect Password";
}
}
return dispatch;
}
const a = make_account(100, "eva");
a("withdraw", "eva")(50); //withdraws 50
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.
function call_the_cops(reason) {
return "calling the cops because " + reason;
}
function make_account(balance, p) {
let invalid_attempts = 0; //initializes to 0
function withdraw(amount) {
if (balance >= amount) {
balance = balance - amount;
return balance;
} else {
return "Insufficient funds";
}
}
function deposit(amount) {
balance = balance + amount;
return balance;
}
function calling_the_cops(_) {
return call_the_cops("you have exceeded " +
"the max no of failed attempts");
}
function dispatch(m, q) {
if (invalid_attempts < 7) {
if (p === q) {
return m === "withdraw"
? withdraw
: m === "deposit"
? deposit
: "Unknown request: make_account";
} else {
invalid_attempts = invalid_attempts + 1;
return x => "Incorrect Password";
}
} else {
return calling_the_cops;
}
}
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, 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.
The
value of an assignment is the value being assigned to the name.
Assignment expression statements
look similar to and should not be
confused with constant and variable declarations of the form
const $name$ = $value$;
and
let $name$ = $value$;
in which a newly declared $name$
is associated with a $value$.
Assignment expressions 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
JavaScript the body block
of a function can contain a sequence of function declarations
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]
Blocks as bodies of lambda expressions were
introduced in section 2.2.4.
[7]
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.
[8]
In contrast with
new-withdrawmake_withdraw_balance_100
above, we do not have to use
let
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.)