2 Looking for the Self
In the previous section, we have built a simple object system, whose main limitation is that objects are not "aware" of themselves. Object-oriented programming languages all give this self-awareness to objects, which allows methods to reuse others, and to be (mutually) recursive.
Consider a simple extension of our counters from the previous chapter with an inc-by! method, which does two things: it changes the step by its argument (like step!) and then actually increments the counter (like inc). Of course we’d want to implement this "compound" method by invoking the existing methods on the currently-executing object:
(method inc-by! (v) (-> self step! v) (-> self inc))
Here we used self to denote the currently-executing object; in other languages, like Java and JavaScript, it is called this. Clearly, our account of OOP so far does not inform us as to what self is.
2.1 What is Self?
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 counter (letrec ([self (let ([count 0] [step 1]) (let ([methods (list (cons 'inc (λ () (set! count (+ count step)) count)) (cons 'dec (λ () (set! count (- count step)) count)) (cons 'reset (λ () (set! count 0))) (cons 'step! (λ (v) (set! step v))) (cons 'inc-by! (λ (n) (self 'step! n) (self 'inc))))]) (λ (msg . args) (let ([found (assoc msg methods)]) (if found (apply (cdr found) args) (error "message not understood:" msg))))))]) self))
> (counter 'inc) 1
> (counter 'inc-by! 20) 21
> (counter 'inc) 41
2.2 Self with Macros
Let us take the pattern above and use it in our OBJECT macro:
(defmac (OBJECT ([field fname fval] ...) ([method mname mparams mbody ...] ...)) #:keywords field method (letrec ([self (let ([fname fval] ...) (let ([methods (list (cons 'mname (λ mparams mbody ...)) ...)]) (λ (msg . args) (let ([found (assoc msg methods)]) (if found (apply (cdr found) args) (error "message not understood:" msg))))))]) self)) (defmac (→ o m arg ...) (o 'm arg ...))
(define (make-counter [init-count 0] [init-step 1]) (OBJECT ([field count init-count] [field step init-step]) ([method inc () (set! count (+ count step)) count] [method dec () (set! count (- count step)) count] [method reset () (set! count 0)] [method step! (v) (set! step v)] [method inc-by! (v) (→ self step! v) (→ self inc)])))
> (let ([c (make-counter)]) (→ c inc-by! 20)) self: undefined;
cannot reference an identifier before its definition
in module: 'program
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 inc-by! 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 fval] ...) ([method mname mparams mbody ...] ...)) #:keywords field method #:captures self (letrec ([self (let ([fname fval] ...) (let ([methods (list (cons 'mname (λ mparams mbody ...)) ...)]) (λ (msg . args) (let ([found (assoc msg methods)]) (if found (apply (cdr found) args) (error "message not understood:" msg))))))]) self))
> (let ([c (make-counter)]) (→ c inc-by! 20)) 20
2.3 Mutually-Recursive Methods
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! Can you explain why?
(define odd-even (OBJECT () ([method even (n) (match n [0 #t] [1 #f] [else (→ self odd (- n 1))])] [method odd (n) (match n [0 #f] [1 #t] [else (→ self even (- n 1))])])))
> (→ odd-even odd 15) #t
> (→ odd-even odd 14) #f
> (→ odd-even even 1423842) #t
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.
2.4 Nested Objects
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 counter-factory (OBJECT ([field default-count 0] [field default-step 1]) ([method df-count! (v) (set! default-count v)] [method df-step! (v) (set! default-step v)] [method make () (OBJECT ([field count default-count] [field step default-step]) ([method inc () (set! count (+ count step)) count] [method dec () (set! count (- count step)) count] [method reset () (set! count 0)] [method step! (v) (set! step v)] [method inc-by! (v) (→ self step! v) (→ self inc)]))])))
> (define c1 (→ counter-factory make)) > (→ c1 inc) 1
> (→ counter-factory df-count! 10) > (→ counter-factory df-step! 5) > (→ c1 inc) 2
> (define c2 (→ counter-factory make)) > (→ c2 inc) 15
> (→ c1 inc) 3
> (→ c2 inc) 20