Let's take a quick glance at what we will be going over in this section:
(define (make-account balance)
(define (withdraw amount)
(set! balance (- balance amount)) balance)
(define (deposit amount)
(set! balance (+ balance amount)) balance)
(define (dispatch msg)
(cond
((eq? msg 'withdraw) withdraw)
((eq? msg 'deposit) deposit) ) )
dispatch)
What do you think? Do you have any idea about what this function does?
Let's withdraw money from the bank account. We will do this using a procedure
withdraw
, 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
:
(withdraw 25)
75
(withdraw 25)
50
(withdraw 60)
"Insufficient funds"
(withdraw 15)
35
Observe that the expression (withdraw 25)
, evaluated twice, yields different
values.
Wait, but I thought that a particular function call with the same argument returns the same value!
Up to now it has, but this is a new kind of behavior for a procedure. All our procedures could be viewed as functions that pass the vertical line test. A call to a procedure computes the value of the function applied to the given arguments, and two calls to the same procedure with the same arguments always produces the same result. But in this situation, the balance needs to be changed after each transaction. Otherwise, we all are going to be rich!
To implement withdraw
, we can use a variable balance
to indicate the
balance of money in the account and define withdraw
as a procedure that
accesses balance. The withdraw procedure 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 definitions of balance
and
withdraw
:
(define balance 100)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
Decrementing balance is accomplished by the expression
(set! balance (- balance amount))
This uses the set!
special form, whose syntax is
(set! [name] [new-value])
Here [name]
is a symbol and [new-value]
is any expression. Set!
changes
[name]
so that its value is the result obtained by evaluating [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.
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 [exp1] [exp2] ... [expk])
causes the expressions [exp1]
through [expk]
to be evaluated in sequence
and the value of the final expression [expk]
to be returned as the value of
the entire begin
form.
Play with withdraw
, set!
and begin
on your STk interpreter!
Before we move on, examine again how withdraw
and balance
are defined:
(define balance 100)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
Do you see anything that could cause a trouble?
The problem is with the variable balance
. As specified above, balance
is a name defined in the global environment and is freely accessible to be
examined or modified by any procedure. It would be much better if we could
somehow make balance
internal to withdraw
, so that withdraw
would be
the only procedure that could access balance
directly and any other
procedure 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:
(define new-withdraw
(let ((balance 100))
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))))
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.
> (new-withdraw 10)
90
> (new-withdraw 30)
60
Play with new-withdraw
on the STk interpreter and make sure you understand how it works.
make-account
Here is a simplified version of the make-account
procedure in SICP:
(define (make-account balance)
(define (withdraw amount)
(set! balance (- balance amount)) balance)
(define (deposit amount)
(set! balance (+ balance amount)) balance)
(define (dispatch msg)
(cond ((eq? msg 'withdraw) withdraw)
((eq? msg 'deposit) deposit) ) )
dispatch)
Now, let's try to rewrite this using local state variables. Fill in the blank in the following code so that the result works exactly the same as the make-account
procedure above. That is, it responds to the same messages and produces the same return values. The differences between the two procedures are that the inside of make-account
above is enclosed in the let
statement below, and the names of the parameters to make-account
are different.
(define (make-account init-amount)
(let (______________________)
(define (withdraw amount)
(set! balance (- balance amount)) balance)
(define (deposit amount)
(set! balance (+ balance amount)) balance)
(define (dispatch msg)
(cond ((eq? msg 'withdraw) withdraw)
((eq? msg 'deposit) deposit) ) )
dispatch) )
Now, modify either version of make-account so that, given the message balance, it returns the current account balance, and given the message init-balance, it returns the amount with which the account was initially created. For example,
> (define acc (make-account 100))
acc
> (acc 'balance)
100
Make another modification such that, given the message transactions (any deposit or withdrawal), it returns a list of all transactions made since the account was opened. For example:
> (define acc (make-account 100))
acc
> ((acc 'withdraw) 50)
50
> ((acc 'deposit) 10)
60
> (acc 'balance)
60
> (acc 'transactions)
((deposit 10) (withdraw 50))
Before viewing the entire solution below, try out your definition in the STk interpreter and make sure you understand the entire code for make-account
.
Here is our solution:
(define (make-account init-amount)
(let ((balance init-amount)
(transactions '()))
(define (withdraw amount)
(set! balance (- balance amount))
(set! transactions (cons (list 'withdraw amount) transactions))
balance)
(define (deposit amount)
(set! balance (+ balance amount))
(set! transactions (cons (list 'deposit amount) transactions))
balance)
(define (dispatch msg)
(cond ((eq? msg 'withdraw) withdraw)
((eq? msg 'deposit) deposit)
((eq? msg 'balance) balance)
((eq? msg 'transactions) transactions) ) )
dispatch) )
Given this definition:
(define (plus1 var)
(set! var (+ var 1))
var)
Follow the substitution model to find the result of computing
(plus1 5)
That is, show the expression that results from substituting 5
for var
in the body of plus1
, and then compute the value of the resulting expression.
Now, try it in the STk interpreter. Did you get the same answer? Why or why not?
Introducing assignments accompanies a pretty big cost. At this point, you may realize that we cannot use the substitution model of evaluation anymore because it yields the wrong value. The trouble here is that substitution is based ultimately on the notion that the symbols in our language are essentially names for values. But as soon as we introduce set!
and the idea that the value of a variable can change, a variable can no longer be simply a name. Now a variable somehow refers to a place where a value can be stored, and the value stored at this place can change.
Then how can I evaluate the procedures?
The new model of evaluation is waiting for you in the next subsection.
In this section, you learned:
set!
and begin
Let's go to the next subsection and learn about the new model of evaluation!