Lectures
Lecture 1: The Essence of Objects
Lecture 2: Unions of Objects
Lecture 3: Classes of Objects:   Data Definitions
Lecture 4: Classes of Objects:   Interface Definitions
Lecture 5: Interface Design:   Independent and Extensible
Lecture 6: Parametric Interface Definitions and Methods
Lecture 7: Introducing Java:   Syntax and Semantics
Lecture 8: Union, Interfaces, and Lists in Java
Lecture 9: Testing in Java
Lecture 10: Parametric Interfaces in Java
Lecture 11: Computations on Many Structural Arguments:   Double Dispatch
Lecture 12: Parameterized Types and Double Dispatch; Abstracting Values
Lecture 13: Abstracting Computation with Function Objects
Lecture 14: Function Objects & Parameterized Types; Anonymous Classes & Lambda
Lecture 15: The Fundamental List Abstraction:   Fold
Lecture 17: Midterm Review
Lecture 16: Properties of Equality:   Reflexive, Symmetric, Transitive, and Total
Lecture 19: Structural Equality with Double Dispatch; Abstracting and Overridding
Lecture 18: More Double Dispatch
Lecture 22: Optional, Maps, Sets, and Lifting Default Code to Abstract Classes
Lecture 23: The Visitor Pattern
Lecture 24: Implementing Visitors; Bank Accounts
Lecture 25: Imperatives:   Implicit Communication via Side-Effects
Lecture 26: Aside:   List Exercises
Lecture 27: Imperatives:   Cyclic Data
Lecture 28: Imperatives:   Methods over Cylic Data
Lecture 29: BSTs, Maps, The Law of Hash  Code, and Comparable vs Comparators
Lecture 30: Random access and Array  Lists
Lecture 31: Implementing Hash Tables
Lecture 32: Resizing Hash Tables
Lecture 33: Simple Iterators
Lecture 34: List Iterators and Iterator Combinators
Lecture 35: List Iterators and Iterator Combinators
Lecture 36: Zippers
Lecture 37: Naive Tree Iterators
Lecture 38: Efficient Pre-Order Tree Iterators
Lecture 39: Drills
Lecture 40: Drill Solutions
Lecture 41: Wrap-up
On this page:
1 Atomic and Compound Data
2 Enumerations
3 Unions and Recursive Unions
6.12

Lecture 3: Classes of Objects: Data Definitions

Video 2019. Video 2018.

One of the most important lessons of How to Design Programs is that the structure of code follows the structure of the data it operates on, which means that the structure of your code can be derived systematically from your data definitions. In this chapter, we see how to apply the design recipe to design data represented using classes as well as operations implemented as methods in these classes.

We’ve seen various kinds of data definitions:
  1. Atomic: numbers, images, strings, ...

  2. Compound: structures, posns, ...

  3. Enumerations: colors, key events, ...

  4. Unions: atoms, ...

  5. Recursive unions: trees, lists, matryoshka dolls, s-expressions, ...

  6. Functions: infinite sets, sequences, ...

Each of these kinds of data definitions can be realized with objects. In this chapter, we’ll examine how each the first five are implemented with a class-based design. We’ll return to representing functions later.

1 Atomic and Compound Data

In Lecture 1: The Essence of Objects, we’ve already seen how to represent compound data as an object. We can do the same for atomic data by considering like a structure with one field; a design we might’ve consider superfluous last semester, but which makes sense once we combine data and functionality into objects.

Stepping back, we can see that the way to represent some fixed number N of data is with a class with N fields. For example, a position can be represented by a pair (x,y) of real numbers:

;; A Posn is (new coord% Real Real)
(define-class coord%
  (fields x y))

Methods can compute with any given arguments and the object that calling the method, thus the template for a coord% method is:

;; coord%-method : Z ... -> ???
(define (coord%-method z ...)
  (... (send this x) (send this y) z ...))

Here we see that our template lists the available parts of the coord% object, in particular the two fields x and y.

2 Enumerations

An enumeration is a data definition for a finite set of possibilities. For example, we can represent a traffic light like the ones on Baltimore Avenue with a finite set of strings, as we did in SPD I:

;; A Light is one of:
;; - "Red"
;; - "Green"
;; - "Yellow"

Following the design recipe, we can construct the template for functions on Lights:

;; light-function : Light -> ???
(define (light-function l)
  (cond [(string=? "Red" l) ...]
        [(string=? "Green" l) ...]
        [(string=? "Yellow" l) ...]))

Finally, we can define functions over Lights, following the template.
;; next : Light -> Light
;; Next light after the given light
(check-expect (next "Green") "Yellow")
(check-expect (next "Red") "Green")
(check-expect (next "Yellow") "Red")
(define (next l)
  (cond [(string=? "Red" l) "Green"]
        [(string=? "Green" l) "Yellow"]
        [(string=? "Yellow" l) "Red"]))

That’s all well and good for a function-oriented design, but we want to design this using classes, methods, and objects.

There are two obvious possibilities. First, we could create a light% class, with a field holding a Light. However, this fails to use classes and objects to their full potential. Instead, we will design a class for each state the traffic light can be in. Each of the three classes will have their own implementation of the next method, producing the appropriate Light.

#lang class/0
;; A Light is one of:
;; - (new red%)
;; - (new green%)
;; - (new yellow%)
 
(define-class red%
  ;; next : -> Light
  ;; Next light after red
  (check-expect (send (new red%) next) (new green%))
  (define (next)
    (new green%)))
 
(define-class green%
  ;; next : -> Light
  ;; Next light after green
  (check-expect (send (new green%) next) (new yellow%))
  (define (next)
    (new yellow%)))
 
(define-class yellow%
  ;; next : -> Light
  ;; Next light after yellow
  (check-expect (send (new yellow%) next) (new red%))
  (define (next)
    (new red%)))

If you have a Light, L, how do you get the next light?

(send L next)

Note that there is no use of cond in this program, although the previous design using functions needed a cond because the next function has to determine what kind of light is the given light. However in the object-oriented version there’s no use of a cond because we ask an object to call a method; each kind of light has a different next method that knows how to compute the appropriate next light. Notice how the purpose statements are revised to reflect knowledge based on the class the method is in; for example, the next method of yellow% knows that this light is yellow.

3 Unions and Recursive Unions

Unions are a generalization of enumerations to represent infinite families of data. We saw simple (non-recursive) unions in Lecture 2: Unions of Objects, but let’s consider recursive unions now. One example is binary trees, which can contain arbitrary other data as elements. We’ll now look at how to model binary trees of numbers, such as:

  7         6              8

           / \            / \

          8   4          2   1

             / \

            3   2

How would we represent this with classes and objects?

#lang class/0
;;   +- - - - - - - - - - - - - - +
;;   | +- - - - - - - - - - - - + |
;;   V V                        | |
;; A BT is one of:              | |
;; - (new leaf% Number)         | |
;; - (new node% Number BT BT)   | |
;;                     |  +- - -+ |
;;                     +- - - - --+
(define-class leaf%
  (fields number))
 
(define-class node%
  (fields number left right))
 
(define ex1 (new leaf% 7))
(define ex2 (new node% 6
                 (new leaf% 8)
                 (new node% 4
                      (new leaf% 3)
                      (new leaf% 2))))
(define ex3 (new node% 8
                 (new leaf% 2)
                 (new leaf% 1)))

We then want to design a method count which produces the number of numbers stored in a BT.

Here are our examples:

(check-expect (send ex1 count) 1)
(check-expect (send ex2 count) 5)
(check-expect (send ex3 count) 3)

Next, we write down the templates for methods of our two classes.

The template for leaf%:

leaf%

;; count : -> Number
;; count the number of numbers in this leaf
(define (count)
  (... (send this number) ...))

The template for node%:

node%

;; count : -> Number
;; count the number of numbers in this node
(define (count)
  (send this number) ...
  (send (send this left) count) ...
  (send (send this right) count) ...)

Now we provide a definition of the count method for each of our classes.

leaf%

;; count : -> Number
;; count the number of numbers in this leaf
(define (count)
  1)

node%

;; count : -> Number
;; count the number of numbers in this node
(define (count)
  (+ 1
     (send (send this left) count)
     (send (send this right) count)))

Next, we want to write the double function, which takes a number and produces two copies of the BT with the given number at the top. Here is a straightforward implementation for leaf%:

leaf%

;; double : Number -> BT
;; double this leaf and put the number on top
(define (double n)
  (new node%
       n
       (new leaf% (send this number))
       (new leaf% (send this number))))

Note that (new leaf% (send this number)) is just constructing a new leaf% object just like the one we started with. Fortunately, we have a way of referring to ourselves, using the identifier this. We can thus write the method as:

leaf%

;; double : Number -> BT
;; double this leaf and put the number on top
(define (double n)
  (new node% n this this))

For node%, the method is very similar:

Since these two methods are so similar, you may wonder if they can be abstracted to avoid duplication. We will see how to do this in a subsequent class.

node%

;; double : Number -> BT
;; double this node and put the number on top
(define (double n)
  (new node% n this this))

The full BT code is now:
#lang class/0
;;   +- - - - - - - - - - - - - - +
;;   | +- - - - - - - - - - - - + |
;;   V V                        | |
;; A BT is one of:              | |
;; - (new leaf% Number)         | |
;; - (new node% Number BT BT)   | |
;;                     |  +- - -+ |
;;                     +- - - - --+
(define-class leaf%
  (fields number)
  ;; count : -> Number
  ;; count the number of numbers in this leaf
  (define (count)
    1)
 
  ;; double : Number -> BT
  ;; double the leaf and put the number on top
  (define (double n)
    (new node% n this this)))
 
(define-class node%
  (fields number left right)
  ;; count : -> Number
  ;; count the number of numbers in this node
  (define (count)
    (+ 1
       (send (send this left) count)
       (send (send this right) count)))
 
  ;; double : Number -> BT
  ;; double the node and put the number on top
  (define (double n)
    (new node% n this this)))
 
(define ex1 (new leaf% 7))
(define ex2 (new node% 6
                 (new leaf% 8)
                 (new node% 4
                      (new leaf% 3)
                      (new leaf% 2))))
(define ex3 (new node% 8
                 (new leaf% 2)
                 (new leaf% 1)))
 
(check-expect (send ex1 count) 1)
(check-expect (send ex2 count) 5)
(check-expect (send ex3 count) 3)
 
(check-expect (send ex1 double 5)
              (new node% 5 ex1 ex1))
(check-expect (send ex3 double 0)
              (new node% 0 ex3 ex3))