Lecture 2: Unions of Objects
We’ve already seen the fundamental idea of objects, that an object is the pairing together of data and functionality. Over the next few lectures we will explore what that means in terms of program designs that we’re familiar with.
(In class, we reviewed the basics of how objects and method invocation work for a simple example of compound data: coordinates. These topics are covered in Lecture 1: The Essence of Objects.)
Let’s take a look at a data definition that involves a union. First, let’s look at the design we are familar with:
;; A Shape is one of: ;; - (make-circ Real) ;; - (make-sq Real) ;; Interp: either a circle or a square (define-struct circ (radius)) (define-struct sq (size))
Here we are defining a set of values called Shape and a Shape is either a circle or a square. Circles are constructed using the make-circ constructor, given a radius for the circle, and squares are constructed using the make-sq constructor giving the size of the edge of the square.
Knowing the data definition, we can write the template for any function that operates on shapes. The template consists of a cond to determine which variant of the union was given as the argument to the function, and then within each branch of the cond, the function can deconstruct the structure by accessing its fields:
;; shape-template : Shape -> ?? (define (shape-template s) (cond [(circ? s) (... (circ-radius s) ...)] [(sq? s) (... (sq-size s) ...)]))
Now let’s write a particular function on shapes, the function for computing the area of a shape:
;; area : Shape -> Real ;; Compute the area of the given shape (check-expect (area (make-sq 3)) 9) (check-within (area (make-circ 5)) (* 25 pi) 0.0001) (define (area s) (cond [(circ? s) (* pi (sqr (circ-radius s)))] [(sq? s) (sqr (sq-size s))]))
This is all familiar stuff. Let’s consider what happens when we switch to using objects.
The first observation to make is that in the above code, we define the concept of Shape, but there’s no Shape structure. There are only circ and sq structures. Analogously, there will not be a Shape class, but rather we will define Shape as the union of classes for circles and squares. Designing the class-based analogs of circ and sq is pretty straightforward using the ideas we saw in Lecture 1: The Essence of Objects for representing compound data with objects.
Focusing just on the data definition (not the functionality yet), we get:
;; A Shape is one of: ;; - (new circ% Real) ;; - (new sq% Real) ;; Interp: either a circle or a square (define-class circ% (fields radius)) (define-class sq% (fields size))
Next we can think about the method for area. If we think about writing a method for each kind of shape, the problem is pretty simple. We can write a method computing the area of a circle as follows:
circ%
;; area : -> Real ;; Compute the area of this circle (define (area) (* pi (sqr (send this radius))))
A note on formatting: we use the above convention to mean that the code belongs inside the circ% class definition. In other words, we’re really saying:
(define-class circ% (fields radius) ;; area : -> Real ;; Compute the area of this circle (define (area) (* pi (sqr (send this radius)))))
The area method for squares is straightforward too:
sq%
;; area : -> Real ;; Compute the area of this square (define (area) (sqr (send this size)))
Now we have methods for computing the area of squares and the area of circles, but what about Shapes? Well, we’re actually done. Consider having some value s which you know to be a Shape. You can compute its area by sending it the area method name, i.e. (send s area).
A natural thing to wonder is which method will be used? The answer is (just like in the ISL code) “it depends on what kind of shape s is.” If s was constructed with the circ% constructor, it will use the circle method for area; if it was constructed with sq%, it will use the square one. The key difference, compared with the structure and function approach is that we, the programmer, don’t have to explicitly write out the case analysis – the cond disappears in the object-oriented design.
So if the cond disappears, how does the object know which method to use? The answer here comes back to the fundamental idea of objects. Since the object has its functionality coupled together with its data it doesn’t “figure out” which method to use, it simply uses the method that is part of the object.
It will take some getting used to this new style, but this idea lies at the heart of programming in an object-oriented style. Instead of using functions which are independent of data and must explicitly inspect how the data was constructed in order to figure out what to compute, we instead couple the appropriate computation with the data in an object.
Finally, it’s worth noting the subtle difference in the signatures and purpose statements for the object-oriented program (in particular we go from a single signature and purpose statement for the area function to two for the area methods).