set! introduces time into the model. Once variables can change, substitution breaks down. Objects with local state model real-world entities but sacrifice referential transparency.
Local state variables
make-withdraw returns a procedure that remembers its balance. Each call to the returned procedure mutates the enclosed variable. The procedure has state.
Scheme
; A withdraw procedure with local state
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
(define W (make-withdraw 100))
(display (W 25)) (newline) ; 75
(display (W 25)) (newline) ; 50
(display (W 60)) (newline) ; Insufficient funds
(display (W 15)) ; 35
Without set!, calling a procedure with the same arguments always returns the same result. That is referential transparency. Assignment destroys it: (W 25) returns different values on successive calls. The substitution model cannot explain this; we need the environment model (next chapter).
Scheme
; Referential transparency: pure version; Same input always gives same output
(define (make-decrementer balance)
(lambda (amount) (- balance amount)))
(define D (make-decrementer 100))
(display (D 25)) (newline) ; always 75
(display (D 25)) (newline) ; always 75; Now compare with set! version:; Same call, different results
(define (make-withdraw balance)
(lambda (amount)
(if (>= balance amount)
(begin (set! balance (- balance amount))
balance)
"Insufficient funds")))
(define W (make-withdraw 100))
(display (W 25)) (newline) ; 75
(display (W 25)) ; 50 โ not the same!
Python
# Pure: same input, same outputdef decrement(balance, amount):
return balance - amount
print(decrement(100, 25)) # always 75print(decrement(100, 25)) # always 75# Stateful: same call, different resultdef make_withdraw(balance):
b = [balance]
def withdraw(amount):
if b[0] >= amount:
b[0] -= amount
return b[0]
return"Insufficient funds"return withdraw
W = make_withdraw(100)
print(W(25)) # 75print(W(25)) # 50 โ not the same!
The benefits of assignment: Monte Carlo
Assignment enables clean modular design when modeling processes with internal randomness. The Monte Carlo method estimates π by testing random points in the unit square. The random number generator hides its state; the estimator does not need to know how randomness works.
If two variables refer to the same mutable object, changing one affects the other. Before set!, "same" meant "equal value." After set!, we must distinguish identity (same object) from equality (same value).
Scheme
; Identity vs equality with mutable 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)
(define acc1 (make-account 100))
(define acc2 (make-account 100))
; acc1 and acc2 have equal balances but different identity
((acc1 'withdraw) 30)
((acc2 'withdraw) 10)
; acc1 has 70, acc2 has 90 โ same starting value, different state
(display "acc1: ") (display ((acc1 'withdraw) 0)) (newline)
(display "acc2: ") (display ((acc2 'withdraw) 0)) (newline)
; Aliasing: acc3 IS acc1
(define acc3 acc1)
((acc3 'deposit) 50)
(display "acc1 after acc3 deposit: ")
(display ((acc1 'withdraw) 0))