On this page:
2.1 What is Self?
2.2 Self with Macros
2.3 Points with Self
2.4 Mutually-Recursive Methods
2.5 Nested Objects

2 Looking for the Self

Éric Tanter

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.

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 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))
Note that the body of the letrec simply returns self, which is bound to the recursive procedure we have defined.

> ((point 'x! 10) 'x?)

10

In Smalltalk, methods return self by default.

Note how the fact that the setter method x! returns self allow us to chain message sends.

2.2 Self with Macros

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 ...))

Now let us try it out with some points:
(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))

self: undefined;

 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))

2.3 Points with Self

We can now define various methods that return and/or use self in their bodies:
(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?)

5

> (-> (-> p1 move 1 1) x?)

6

2.4 Mutually-Recursive Methods

The previous section already shows that methods can use other methods by sending messages to self. This other example shows 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!

(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)

#t

> (-> odd-even odd 14)

#f

> (-> odd-even even 14)

#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.5 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.

Consider the following example:
(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)

10

> (-> factory factor! 2)
> (-> o1 val)

20

> (-> factory price! 20)
> (-> o1 val)

20

> (define o2 (-> factory make))
> (-> o2 val)

40

Can you do the same in Java?

Convince yourself that these results make sense.