In the previous section, we have built a simple object system. Now consider that we want to define a method on point objects, called above, which takes another point as parameter and returns the one that is higher (wrt the y-axis):
(method above (other-point) (if (> (-> other-point y?) y) other-point self))
Note that we intuitively used self to denote the currently-executing object; in other languages it is sometimes called this. Clearly, our account of OOP so far does not inform us as to what self is.
Let us go back to our first definition of an object (without macros). We see that an object is a function; and so we want, from within that function, to be able to refer to itself. How do we do that? We already know the answer from our study on recursion! We just have to use a recursive binding (with letrec) to give a name to the function-object and then we can use it in method definitions:
(define point (letrec ([self (let ([x 0]) (let ([methods (list (cons 'x? (λ () x)) (cons 'x! (λ (nx) (set! x nx) self)))]) (λ (msg . args) (apply (cdr (assoc msg methods)) args))))]) self))
> ((point 'x! 10) 'x?)
In Smalltalk, methods return self by default.
Let us take the pattern above and use it in our OBJECT macro:
(defmac (OBJECT ([field fname init] ...) ([method mname args body] ...)) #:keywords field method (letrec ([self (let ([fname init] ...) (let ([methods (list (cons 'mname (λ args body)) ...)]) (λ (msg . vals) (apply (cdr (assoc msg methods)) vals))))]) self)) (defmac (-> o m arg ...) (o 'm arg ...))
(define (make-point init-x) (OBJECT ([field x init-x]) ([method x? () x] [method x! (nx) (set! x nx)] [method greater (other-point) (if (> (-> other-point x?) x) other-point self)])))
> (let ([p1 (make-point 5)] [p2 (make-point 2)]) (-> p1 greater p2))
cannot reference undefined identifier
What?? But we did introduce self with letrec, so why isn’t it defined? The reason is... because of hygiene! Remember that Scheme’s syntax-rules is hygienic, and for that reason, it transparently renames all identifiers introduced by macros such that they don’t accidentally capture/get captured where the macro is expanded. It is possible to visualize this very precisely using the macro stepper of DrRacket. You will see that the identifier self in the greater method is not the same color as the same identifier in the letrec expression.
Luckily for us, defmac supports a way to specify identifiers that can be used by the macro user code even though they are introduced by the macro itself. The only thing we need to do is therefore to specify that self is such an identifier:
(defmac (OBJECT ([field fname init] ...) ([method mname args body] ...)) #:keywords field method #:captures self (letrec ([self (let ([fname init] ...) (let ([methods (list (cons 'mname (λ args body)) ...)]) (λ (msg . vals) (apply (cdr (assoc msg methods)) vals))))]) self))
(define (make-point init-x init-y) (OBJECT ([field x init-x] [field y init-y]) ([method x? () x] [method y? () y] [method x! (new-x) (set! x new-x)] [method y! (new-y) (set! y new-y)] [method above (other-point) (if (> (-> other-point y?) y) other-point self)] [method move (dx dy) (begin (-> self x! (+ dx (-> self x?))) (-> self y! (+ dy (-> self y?))) self)]))) (define p1 (make-point 5 5)) (define p2 (make-point 2 2))
> (-> (-> p1 above p2) x?)
> (-> (-> p1 move 1 1) x?)
Try the same definition in Java, and compare the results for "large" numbers. Yes, our small object system does enjoy the benefits of tail-call optimization!
(define odd-even (OBJECT () ([method even (n) (case n [(0) #t] [(1) #f] [else (-> self odd (- n 1))])] [method odd (n) (case n [(0) #f] [(1) #t] [else (-> self even (- n 1))])])))
> (-> odd-even odd 15)
> (-> odd-even odd 14)
> (-> odd-even even 14)
We now have an object system that supports self, including returning self, and sending messages to self. Notice how self is bound in methods at object creation time: when the methods are defined, they capture the binding of self and this binding is fixed from then on. We will see in the following chapters that this eventually does not work if we want to support delegation or if we want to support classes.
Because objects and methods are compiled into lambdas in Scheme, our objects inherit interesting properties. First, as we have seen, they are first-class values (otherwise what would be the point?). Also, as we have just seen above, method invocations in tail position are treated as tail calls, and therefore space efficient. We now look at another benefit: we can use higher-order programming patterns, such as objects producing objects (usually called factories). That is, we can define nested objects, with proper lexical scoping.
(define factory (OBJECT ([field factor 1] [field price 10]) ([method factor! (nf) (set! factor nf)] [method price! (np) (set! price np)] [method make () (OBJECT ([field price price]) ([method val () (* price factor)]))])))
> (define o1 (-> factory make)) > (-> o1 val)
> (-> factory factor! 2) > (-> o1 val)
> (-> factory price! 20) > (-> o1 val)
> (define o2 (-> factory make)) > (-> o2 val)
Can you do the same in Java?