Lecture 3: Classes of Objects: Data Definitions
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.
Atomic: numbers, images, strings, ...
Compound: structures, posns, ...
Enumerations: colors, key events, ...
Unions: atoms, ...
Recursive unions: trees, lists, matryoshka dolls, s-expressions, ...
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) ...]))
;; 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))
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))
#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))