Spring 1996
Using objects to represent data is an important part of a widely-used programming style called ``object-oriented programming''.
An object has two primary characteristics:
One advantage of such local state is that it cannot be examined or modified, either intentionally or accidentally, by code outside the object. Careless or malicious modification could corrupt data structures, and sometimes we want to hide local state (as when generating pseudo-random numbers or when checking a password).
Another advantage of local state over global variables is replication: we can have multiple local copies of a variable instead of one global copy. That means that we can model multiple bank accounts, etc., without being forced to copy code.
Some people also favor local state because it avoids introducing many global variables that others might unintentionally reuse when writing their code--if programmers use local variables instead, their programs are less likely to interact badly.
In order to create local state, we place
outside
local
. Use Donkey to examine the difference between
lambda
(define multiplier (local ((define so-far 1)) (lambda (multiplicand) (set! so-far (* so-far multiplicand)) so-far))) (define multiplier (lambda (multiplicand) (local ((define so-far 1)) (set! so-far (* so-far multiplicand)) so-far)))
We have introduced lists as the fundamental Scheme datatype--it is lists that give Scheme much of its expressiveness, and they make manipulating collections of objects very convenient in Scheme. Functions are so powerful, however, that we don't really need lists in Scheme: we can implement them ourselves, using functions. (We can do similar things with structures, numbers, and most other built-in datatypes.)
A klist is an object that responds to the messages
(returning an arbitrary item), 'kar
(returning another klist), and
'kdr
(returning a truth value). We create klists by calling
the function 'knull?
; the constant kons
represents the
empty klist (as an object). Here are the definitions of these two
procedures:
knull
(define knull (lambda (action) (cond [(eq? action 'knull?) #t] [else (error 'knull "Can't perform action ~s" action)]))) (define kons (lambda (first rest) (cond [(eq? action 'car) first] [(eq? action 'cdr) rest] [(eq? action 'knull?) #f])))
Now you can use
, kons
, 'kar
, 'kdr
,
and 'knull?
much like their built-in counterparts:
knull
(define my-list (kons 'a (kons 17 knull))) (my-list 'knull?) ; => #f ((my-list 'kdr) 'kar) ; => 17
We can soothe those programmers by adding definitions for
,
kar
, and kdr
:
knull?
(define kar (lambda (klist) (klist 'kar))) (define kdr (lambda (klist) (klist 'kdr))) (define knull? (lambda (klist) (klist 'knull?)))These new functions don't do much work--they simply pass the buck by sending a message to the object asking it to perform the stated operation. We call this syntactic sugar because a teaspoon of it helps the semantics go down. Now the uses of Now you can use
kons
, kar
, kdr
, knull?
,
and knull
are indistinguishable from their built-in counterparts:
(define my-list (kons 'a (kons 17 knull))) (knull? (kdr (kdr my-list))) ; => #t (kar my-list) ; => 'a
As an exercise, write a function that takes one argument which is one of
'minimum
: the return value is the smallest
number that the function has been called on so far'maximum
: the return value is the largest
number that the function has been called on so far'average
: the return value is the average of
all the numbers that the function has been called on so far.
Here is one solution:
(define number-statistics (local ((define total 0) (define calls 0) (define min 0) (define max 0)) (lambda (arg) (if (zero? calls) (cond [(number? arg) (set! min arg) (set! max arg) (set! total (+ arg total)) (set! calls (+ 1 calls))] [else (error 'number-statistics "No calls yet")]) (cond [(number? arg) (if (< arg min) (set! min arg)) (if (> arg max) (set! max arg)) (set! total (+ arg total)) (set! calls (+ 1 calls))] [(eq? 'minimum arg) min)) [(eq? 'maximum arg) max)] [(eq? 'average arg) (/ total calls)])))
Let's write a procedure that will create new savings accounts for customers at a new, high-tech bank called IntelliBank. IntelliBank provides its customers with tiny computers the size of credit cards called SmartCards. Each SmartCard manages one savings account (which initially contains $0) and allows a customer to deposit or withdraw money from that account, view the current balance, or view the number of transactions (deposits/withdrawals) made during the current month.
We will represent SmartCards as objects which accept the following commands (represented as lists):
(list 'current-balance) ; tells the SmartCard to return the current balance (list 'transactions) ; tells the SmartCard to return number of transactions (list 'deposit x) ; tells the SmartCard to deposit $$x$ (list 'withdraw x) ; tells the SmartCard to withdraw $$x$
(define make-smartcard (lambda () (local ((define BALANCE 0) (define TRANSACTIONS 0)) (lambda (input) (cond ((eq? (car input) 'current-balance) BALANCE) ((eq? (car input) 'transactions) TRANSACTIONS) ((eq? (car input) 'deposit) (local ((define amount (car (cdr input)))) (if (< amount 0) (error 'smartcard "Can't deposit negative amount ~s" amount)) (set! BALANCE (+ BALANCE amount)) (set! TRANSACTIONS (add1 TRANSACTIONS)))) ((eq? (car input) 'withdraw) (local ((define amount (car (cdr input)))) (if (< amount 0) (error 'smartcard "Can't withdraw negative amount ~s" amount)) (if (< (- BALANCE amount) 0) (error 'smartcard "Can't withdraw ~s, which is more than balance ~s" amount balance)) (set! BALANCE (- BALANCE amount)) (set! TRANSACTIONS (add1 TRANSACTIONS)))))))))
It's a bit awkward to build up a list to pass to our function, and it's awkward to break the list back down to get at its pieces. Instead, we could curry the object, so that operations looked like
(card 'current-balance) (card 'transactions) ((card 'deposit) x) ((card 'withdraw) x)
The body of the function would look like
(lambda (input) (cond ((eq? input 'current-balance) BALANCE) ((eq? input 'transactions) TRANSACTIONS) ((eq? input 'deposit) (lambda (amount) ...)) ((eq? (car input) 'withdraw) (lambda (amount) ...))))