3.1 Assignment and Local State
- The world is populated by independent objects possessing individual, changing state.
- When we say an object “has state,” we mean its behavior is influenced by its history.
- Instead of remembering an object’s history, we can summarize it with state variables.
- For example, the state variable for a bank account would record its current balance.
Indeed, the view that a system is composed of separate objects is most useful when the state variables of the system can be grouped into closely coupled subsystems that are only loosely coupled to other subsystems. (Section 3.1)
- To model state variables in our programming language, we need an assignment operator that changes the value associated with a name.
# 3.1.1 Local State Variables
- Let’s model the situation of withdrawing money from a bank account.
(withdraw amount)
should withdraw the given amount and return the new balance.- If you try to withdraw too much, it should return the string “Insufficient funds.”
- Suppose we begin with $100:
(withdraw 25)
→ 75
(withdraw 25)
→ 50
(withdraw 60)
→ "Insufficient funds"
(withdraw 15)
→ 35
- Notice that
(withdraw 25)
was called twice, but returned different values. - This is a new kind of behavior. Up until now, the returned value of a procedure depended only on the arguments, like a mathematical function.
- Here’s how we implement
withdraw
:
(define balance 100)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
- This uses the special form
(set! name new-value)
to change the value ofbalance
. - The expression
(begin exp1 exp2 ... expn)
evaluates all the expressions in sequence and returns the value of the last one. - Instead of defining
balance
globally, we can encapsulate it withinwithdraw
:
(define withdraw
(let ((balance 100))
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))))
- Unfortunately, the substitution model of evaluation from § 1.1.5 is no longer adequate once we have assignment in our procedures.
- For now, we technically have no way to understand how these procedures work. We will develop a new model soon.
- The following procedure creates “withdraw processors”:
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
(define W1 (make-withdraw 100))
(define W2 (make-withdraw 100))
(W1 50)
→ 50
(W2 70)
→ 30
(W2 40)
→ "Insufficient funds"
(W1 40)
→ 10
W1
andW2
are completely independent objects, each with its own local state variable.- We can also create a “bank-account object” that responds to multiple messages, all operating on the same local state:
(define (make-account balance)
(define (withdraw amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds"))
(define (deposit amount)
(set! balance (+ balance amount))
balance)
(define (dispatch m)
(cond ((eq? m 'withdraw) withdraw)
((eq? m 'deposit) deposit)
(else (error "unknown request" m))))
dispatch)
# 3.1.2 The Benefits of Introducing Assignment
Introducing assignment into our programming language leads us into a thicket of difficult conceptual issues. Nevertheless, viewing systems as collections of objects with local state is a powerful technique for maintaining a modular design. (Section 3.1.2)
- Consider the procedure
rand
, which returns an integer chosen at random. - It’s not clear what “random” means, but we can make a pseudo-random sequence with a seed value
random-init
and a procedurerand-update
that produces the next value:
(define rand
(let ((x random-init))
(lambda ()
(set! x (rand-update x))
x)))
The relation between “real randomness” and so-called pseudo-random sequences, which are produced by well-determined computations and yet have suitable statistical properties, is a complex question involving difficult issues in mathematics and philosophy. (Footnote 3.6)
- One use for randomness is the Monte Carlo method, which finds an approximate solution to a numerical problem by studying random numbers in a probabilistic model.
- We can use the Monte Carlo method to approximate knowing that the probability that two randomly chosen integers have 1 as their GCD is
(define (estimate-pi trials)
(sqrt (/ 6 (monte-carlo trials cesaro-test))))
(define (cesaro-test)
(= (gcd (rand) (rand)) 1))
(define (monte-carlo trials experiment)
(define (iter trials-remaining trials-passed)
(cond ((= trials-remaining 0)
(/ trials-passed trials))
((experiment)
(iter (- trials-remaining 1) (+ trials-passed 1)))
(else
(iter (- trials-remaining 1) trials-passed))))
(iter trials 0))
- If we had to use
rand-update
directly, our Monte Carlo program would betray some painful breaches of modularity.
The general phenomenon illustrated by the Monte Carlo example is this: From the point of view of one part of a complex process, the other parts appear to change with time. They have hidden time-varying local state. If we wish to write computer programs whose structure reflects this decomposition, we make computational objects (such as bank accounts and random-number generators) whose behavior changes with time. We model state with local state variables, and we model the changes of state with assignments to those variables. (Section 3.1.2)
# 3.1.3 The Costs of Introducing Assignment
- The advantages of local state and assignment come at a price: the substitution model of procedure application from § 1.1.5 breaks down. We need a more complex model.
- Programming without the use of assignment, as we did in the first two chapters, is called functional programming.
- Consider a simplified version of the
make-withdraw
procedure from § 3.1.1:
(define (make-simplified-withdraw balance)
(lambda (amount)
(set! balance (- balance amount))
balance))
- Now observe what happens when we try to apply the substitution model to it:
((make-simplified-withdraw 25) 20)
((lambda (amount) (set! balance (- 25 amount)) 25) 20)
(set! balance (- 25 20)) 25
25
- This is the wrong answer. We shouldn’t have substituted 25 for
balance
everywhere, because the assignment changed it. - Before, a variable was simply a name for a value. Now, with assignment, it somehow refers to a place where a value can be stored, and this value can be changed.
# Sameness and change
- A language that supports “equals can be substituted for equals” is referentially transparent. Our language was referentially transparent until we introduced
set!
. - By introducing change into our computational models, many previously straightforward notions become problematic, such as the concept of two things being “the same.”
- If we have
(make-withdraw 25)
and(make-withdraw 25)
, are they the same? No, because they can have different local state. - If we have
(define peter-acc (make-account 100))
, there is a big difference between(define paul-acc (make-account 100))
and(define paul-acc peter-acc)
. - In the first case, they have distinct accounts. In the second, both names are aliased to the same account, so withdrawing from either one will affect the other.
Bugs can occur in our programs if we forget that a change to an object may also, as a “side effect,” change a “different” object because the two “different” objects are actually a single object appearing under different aliases. These so-called side-effect bugs are so difficult to locate and to analyze that some people have proposed that programming languages be designed in such a way as to not allow side effects or aliasing. (Footnote 3.10)
# Pitfalls of imperative programming
- Programming that makes heavy use of assignment is called imperative programming.
- Imperative programs are susceptible to bugs that cannot occur in functional programs.
- Things will get even worse when we throw concurrency into the mix in § 3.4.
In general, programming with assignment forces us to carefully consider the relative orders of the assignments to make sure that each statement is using the correct version of the variables that have been changed. This issue simply does not arise in functional programs. (Section 3.1.3)
In view of this, it is ironic that introductory programming is most often taught in a highly imperative style. This may be a vestige of a belief, common throughout the 1960s and 1970s, that programs that call procedures must inherently be less efficient than programs that perform assignments. … Alternatively it may reflect a view that step-by-step assignment is easier for beginners to visualize than procedure call. Whatever the reason, it often saddles beginning programmers with “should I set this variable before or after that one” concerns that can complicate programming and obscure the important ideas. (Footnote 3.11)