Lab 10
Objects

Comp 210

Spring 1996

Contents

Objects

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:

actions/behavior
The data we have examined so far is passive: it doesn't do anything for itself, but waits to be operated upon by functions. By contrast, an object is active. Rather than being manipulated by external functions, objects respond to messages which request a particular operation; the object performs the requested action for itself.
data hiding/encapsulation
The data we have seen so far have had no secrets: everything there is to know about them can be learned, or changed, by an external party. Objects, by contrast, may have private information which only the object--and no code outside it--may examine and modify.

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 local outside lambda. Use Donkey to examine the difference between

(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)))

Cons cells as objects

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 'kar (returning an arbitrary item), 'kdr (returning another klist), and 'knull? (returning a truth value). We create klists by calling the function kons; the constant knull represents the empty klist (as an object). Here are the definitions of these two procedures:

(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, 'knull?, and knull much like their built-in counterparts:

(define my-list (kons 'a (kons 17 knull)))
(my-list 'knull?) ; => #f
((my-list 'kdr) 'kar) ; => 17

Syntactic sugar

Some programmers find it a bit disconcerting, especially at first, to see the operation follow the object.

We can soothe those programmers by adding definitions for kar, kdr, and 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

Number statistics

As an exercise, write a function that takes one argument which is one of

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)])))

SmartCards

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)))))))))

Currying objects

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)
             ...))))