Lecture 34: Implementing Objects
Compiling objects to purely functional code
32.1 Introduction
Was dynamically typed, like the rest of JavaScript,
Modeled objects as a simple dictionary of property/value pairs,
Used prototypes to implement inheritance, rather than base and derived classes,
Allowed the creation and removal of object properties at runtime (because they’re “just” dictionaries),
The full Racket language actually has a sophisticated object system, which you may be interested in examining later, possibly after completing some more advanced courses in language design.
We will examine two possible representations of objects (there are of course other designs possible), and see how we can implement all the features of an object-oriented language using them. You are strongly encouraged to run the snippets of code in this lecture, and follow along with each version as we add new features.
Do Now!
What features are essential to object-oriented programming?
Creating objects
Properties
Methods
...with arguments
Dynamic dispatch
The notion of this
Some form of inheritance or code-reuse
“An object is what an object has”,
“An object is what an object does”
As our running example, we’ll try to implement Posns, including their distance-to-origin methods, and comparing whether one object is closer to the origin than another.
32.2 “An object is what an object has”
A dictionary of property/value pairs
;; An Object is a [Listof PropVal] ;; A PropVal is a (list Name Value) ;; A Name is a symbol (define my-pos1 (list (list 'x 3) (list 'y 4))) (define my-pos2 (list (list 'x 6) (list 'y 8)))
Do Now!
Design this function, given our current representation.
;; dot :: [Object Name -> Value] (define (dot obj prop) (cond [(empty? obj) (error prop "No such property found")] [(symbol=? (first (first obj)) prop) (second (first obj))] [else (dot (rest obj) prop)])) (check-expect (dot my-pos1 'x) 3) (check-expect (dot my-pos2 'y) 8) (check-error (dot my-pos2 'z) "z: No such property found")
This seems plain enough: our dot function is basically the standard find function, and so we have property lookup.
In practice, these tricky details can be solved, and all compilers for object-oriented languages do use a representation similar to this dictionary-of-properties approach. But understanding those details actually distracts from what’s going on; if you are interested in more information, take a course on compilers!
32.3 “An object is what an object does”
Recall our IList interface that we’ve seen so frequently. If we have an object that we know to be an IList, do we know in
advance what fields it has? No —
Do Now!
What kind of value in ISL also acts like this? What values are what they can do?
;; An Object is [Name -> Value] ;; A Name is a symbol
Do Now!
Define my-pos1 and my-pos2 in this new representation.
(define my-pos1 (λ(prop) (cond [(symbol=? prop 'x) 3] [(symbol=? prop 'y) 4] [else (error prop "No such property found")]))) (define my-pos2 (λ(prop) (cond [(symbol=? prop 'x) 6] [(symbol=? prop 'y) 8] [else (error prop "No such property found")])))
;; dot :: [Object Name -> Value] ;; (1st attempt) (define (dot obj prop) (obj prop)) (check-expect (dot my-pos1 'x) 3) (check-expect (dot my-pos2 'y) 8) (check-error (dot my-pos2 'z) "z: No such property found")
Notice that our tests are completely unchanged: even though we’ve totally changed our representation of objects, we can still interact with them via dot.
32.3.1 Constructors
Do Now!
Do this.
(define (make-pos x y) (λ(prop) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [else (error prop "No such property found")]))) (define my-pos1 (make-pos 3 4)) (define my-pos2 (make-pos 6 8))
32.3.2 Inheritance
Do Now!
Design a make-3d-pos constructor. Can you reuse make-pos in some way?
(define (make-3d-pos x y z) (λ(prop) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'z) z] [else (error prop "No such property found")]))) (define my-pos3 (make-3d-pos 3 4 12)) (check-expect (dot my-pos3 'z) 12) (check-error (dot my-pos 'w) "w: No such property found")
(define (make-3d-pos x y z) (local [(define 2dpos (make-pos x y))] (λ(prop) (cond [(symbol=? prop 'z) z] [else (dot 2dpos prop)])))) (define my-pos3 (make-3d-pos 3 4 12)) (check-expect (dot my-pos3 'z) 12) (check-error (dot my-pos 'w) "w: No such property found")
(define (make-3d-pos x y z) (local [(define super (make-pos x y))] (λ(prop) (cond [(symbol=? prop 'z) z] [else (dot super prop)]))))
(define failure (λ(prop) (error prop "No such property found")))
(define (make-pos x y) (local [(define super failure)] (λ(prop) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [else (dot super prop)]))))
Again, we can rename some of our variables: failure is the simplest possible object, and the one at the root of our object hierarchy. We might well just call it object.
32.3.3 Methods
Do Now!
Design a method 'dist-to-0 that computes the distance to the origin for 2-d points.
(define (make-pos x y) (local [(define super failure)] (λ(prop) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] ;; NEW: [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] [else (dot super prop)])))) (check-expect (dot my-pos1 'dist-to-0) 5) (check-expect (dot my-pos2 'dist-to-0) 10)
Do Now!
If we try to run 'dist-to-0 on my-pos3, we get the wrong answer. Fix this problem.
(define (make-3d-pos x y z) (local [(define super (make-pos x y))] (λ(prop) (cond [(symbol=? prop 'z) z] ;; NEW: [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y) (sqr z)))] [else (dot super prop)]))))
32.3.4 Methods with arguments, this, and dynamic binding
The next level of support for object-oriented features requires a few upgrades all at once, and these can be subtle at first glance. Read through this section slowly, and try stepping through the examples in DrRacket until they make sense.
Do Now!
What’s different about the 'closer-than method as compared to the 'dist-to-0 method?
(define (make-pos x y) (local [(define super failure)] (λ(prop) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] ;; NEW: [(symbol=? prop 'closer-than) (< (dot this 'dist-to-0) (dot that 'dist-to-0))] [else (dot super prop)]))))
;; dot :: [Object Name Params -> Value] (define (dot obj prop args) (obj prop args))
;; An Object is [Name Params -> Value] ;; Params is [Listof Value] (define (make-pos x y) (local [(define super failure)] ;; NEW: args (λ(prop args) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] [(symbol=? prop 'closer-than) ????] [else (dot super prop args)]))))
Do Now!
Now that we have args passed in to our object, how can we define that in the 'closer-than branch?
(define (make-pos x y) (local [(define super failure)] (λ(prop args) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] [(symbol=? prop 'closer-than) ;; NEW: (local [(define that (first args))] (< (dot this 'dist-to-0) (dot that 'dist-to-0)))] [else (dot super prop args)]))))
Do Now!
What value does this refer to in Java? What expression in the ISL code above represents that value?
(define (make-pos x y) (local [(define super object) ;; NEW: (define this (λ(prop args) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] [(symbol=? prop 'closer-than) (local [(define that (first args))] (< (dot this 'dist-to-0) (dot that 'dist-to-0)))] [else (dot super prop args)])))] this))
;; Test 1: (3,4) is closer to the origin than (6,8) (check-expect (dot my-pos1 'closer-than (list my-pos2)) true) ;; Test 2: (6,8) is not closer to the origin than (3,4) (check-expect (dot my-pos2 'closer-than (list my-pos1)) false) ;; Test 3: (6,8) is closer to the origin than (3,4,12) (check-expect (dot my-pos2 'closer-than (list my-pos3)) true) ;; Test 4: (3,4,12) is not closer to the origin than (6,8) (check-expect (dot my-pos3 'closer-than (list my-pos2)) false)
Do Now!
Why?
When we invoke 'closer-than on my-pos2 with an argument of my-pos3, we evaluate whether (dot this 'dist-to-0) is less than (dot that 'dist-to-0), where this is bound to my-pos2, and that is bound to my-pos3.
Evaluating the 'dist-to-0 method on my-pos2 runs the 2-d version of the Pythagorean formula within make-pos, and results in 10. Evaluating 'dist-to-0 on my-pos3 runs the 3-d version within make-3d-pos, and results in 13.
Together, 10 < 13, so we obtain true and the test passes.
When we invoke 'closer-than on my-pos3 with an argument of my-pos2, we find that a 3-d point doesn’t have its own 'closer-than method, so we invoke (dot super prop args), where super is bound to the helper (make-pos 3 4) object. This will invoke 'closer-than on this helper object, with an argument of my-pos2.
When we evaluate 'closer-than on the helper object, that is bound to my-pos2, and this is bound to the helper object, which is (make-pos 3 4).
Evaluating the 'dist-to-0 method on my-pos2 runs the 2-d version of the Pythagorean formula within make-pos, and results in 10. Evaluating 'dist-to-0 on the helper object runs the 2-d version again, and results in 5 (instead of the correct 13).
Together, 5 < 10 is true, so we obtain true and the test fails.
Do Now!
Look carefully at where that is bound. Look carefully at where this is bound. What is the difference?
;; An Object is [Itself Name Params -> Value] ;; A Name is a symbol ;; Params is [Listof Value] ;; Itself is the object on which the method is being invoked (define object (λ(this prop args) (error prop "No such property found"))) (define (make-pos x y) (local [(define super object)] (λ(this prop args) (cond [(symbol=? prop 'x) x] [(symbol=? prop 'y) y] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y)))] [(symbol=? prop 'closer-than) (local [(define that (first args))] (< (dot this 'dist-to-0 '()) (dot that 'dist-to-0 '())))] ;; NEW: [else (super this prop args)])))) (define (make-3d-pos x y z) (local [(define super (make-pos x y))] (λ(this prop args) (cond [(symbol=? prop 'z) z] [(symbol=? prop 'dist-to-0) (sqrt (+ (sqr x) (sqr y) (sqr z)))] ;; NEW: [else (super this prop args)])))) (define (dot obj prop args) ;; Passes the object to itself (obj obj prop args))
;; Test 1: (3,4) is closer to the origin than (6,8) (check-expect (dot my-pos1 'closer-than (list my-pos2)) true) ;; Test 2: (6,8) is not closer to the origin than (3,4) (check-expect (dot my-pos2 'closer-than (list my-pos1)) false) ;; Test 3: (6,8) is closer to the origin than (3,4,12) (check-expect (dot my-pos2 'closer-than (list my-pos3)) true) ;; Test 4: (3,4,12) is not closer to the origin than (6,8) (check-expect (dot my-pos3 'closer-than (list my-pos2)) false)
(You might have noticed that the local definition of this has disappeared. The recursion that we needed hasn’t vanished though: instead, we obtain it by passing the object to itself as its first parameter.)
32.4 Discussion
If we abstract the binding for super and pass it in as an argument to the constructor, we obtain the prototype inheritance pattern of JavaScript.
If instead we abstract just the constructor being called to create super and pass that in instead, we obtain a pattern called mixins, which are essentially “higher-order classes”.
If we do a fair bit of error checking that prop is a symbol, that args is always of the expected length and contains values of the expected kinds, then we essentially are performing runtime type-checking, analogous to the typechecking that Java performs statically.