On this page:
4.1 Message Forwarding
4.2 Delegation
4.3 Programming with Prototypes
4.3.1 Singleton and Exceptional Objects
4.3.2 Sharing through Delegation
4.4 Late Binding of Self and Modularity
4.5 Lexical Scope and Delegation
4.6 Delegation Models
4.7 Cloning

4 Forwarding and Delegation

Éric Tanter

Using message sending, an object can always forward a message to another object in case it does not know how to handle it. With our small object system, we can do that explicitly as follows:

(define seller
 (OBJECT ()
  ([method price (prod)
           (* (case prod
                ((1) ( self price1))
                ((2) ( self price2)))
              ( self unit))]
   [method price1 () 100]
   [method price2 () 200]
   [method unit () 1])))
 
(define broker
 (OBJECT
  ([field provider seller])
  ([method price (prod) ( provider price prod)])))

 

> ( broker price 2)

200

Object broker does not know how to compute the price of a product, but it can claim to do so by implementing a method to handle the price message, and simply forwarding it to seller, who does implement the desired behavior. Note how broker holds a reference to seller in its provider field. This is a typical example of object composition, with message forwarding.

Now, you can see that the problem with this approach is that this forwarding of messages has to be explicit: for each message that we anticipate might be sent to broker, we have to define a method that forwards the message to seller. For instance:
> ( broker unit)

message not understood: 'unit

4.1 Message Forwarding

We can do better by allowing each object to have a special "partner" object to which it automatically forwards any message it does not understand. We can define a new syntactic abstraction, OBJECT-FWD, for constructing such objects:

(defmac (OBJECT-FWD target
                    ([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)
                        (apply target msg args))))))])
    self))

Note how the syntax is extended to specify a target object; this object is used in the dispatch process whenever a message is not found in the methods of the object. Of course, if all objects forward unknown message to some other object, there has to be a last object in the chain, that simply fails when sent a message:

(define root
  (λ (msg . args)
    (error "message not understood" msg)))

Now, broker can be defined simply as follows:

(define broker
  (OBJECT-FWD seller () ()))

That is, broker is an empty object (no fields, no methods) that forwards all messages sent to it to seller:
> ( broker price 2)

200

> ( broker unit)

1

This kind of objects is often called a proxy.

4.2 Delegation

Suppose we want to use broker to refine the behavior of seller; say, we want to double the price of every product, by changing the unit used in the calculation of the prices. This is easy: we just have to define a method unit in broker:

(define broker
  (OBJECT-FWD seller ()
   ([method unit () 2])))
With this definition, we should make sure that asking the price of a product to broker is twice the price of the same product asked to seller:

> ( broker price 1)

100

Hmmm... it does not work! It seems that once we forward the price message to seller, broker never gets the control back; in particular, the unit message that seller sends to self is not received by broker.

Let us consider why this is so. To which object is self bound in seller? To seller! Remember, we said previously (see Looking for the Self) that in our approach, self is statically bound: when an object is created, self is made to refer to the object/closure that is being defined, and will always remain bound to it. This is because letrec, like let, respects lexical scoping.

What we are looking for is another semantics, called delegation. Delegation requires self in an object to be dynamically bound: it should always refer to the object that originally received the message. In our example, this would ensure that when seller sends unit to self, then self is bound to broker, and thus the re-definition of unit in broker takes effect. In that case, we say that seller is the parent of broker, and that broker delegates to its parent.

How do we bind an identifier such that it refers to the value at the point of usage, rather than at the point of definition? In the absence of dynamically-scoped binding forms, the only way we can achieve this is by passing that value as parameter. So, we have to parameterize methods by the actual receiver. Therefore, instead of capturing the self identifier in their surrounding lexical scope, they are parameterized by self.

Concretely, this means that the method:

(λ (prod) .... ( self unit) ....)

in seller must be kept in the methods list as:

(λ (self prod)....( self unit)....)

Ever wondered why methods in Python must explicitly receive self as a first parameter? (In Java, and all other languages, they do as well, although you don’t see it explicitly.)

This parameterization effectively allows us to pass the current receiver after we lookup the method.

Let us now define a new syntactic form, OBJECT-DEL, to support the delegation semantics between objects:
(defmac (OBJECT-DEL parent
                    ([field fname fval] ...)
                    ([method mname (mparam ...) mbody ...] ...))
  #:keywords field method
  #:captures self
  (let ([fname fval] ...)
    (let ([methods
           (list (cons 'mname (λ (self mparam ...) mbody ...)) ...)])
      (λ (current)
        (λ (msg . args)
          (let ([found (assoc msg methods)])
            (if found
                (apply (cdr found) (cons current args))
                (apply (parent current) msg args))))))))

Several things have changed: first, we renamed target to parent, to make it clear that we are defining a delegation semantics. Second, all methods are now parameterized by self, as explained above. Note that we got rid of letrec altogether! This is because letrec was used precisely to allow objects to refer to self, but following lexical scoping. We have seen that for delegation, lexical scoping of self is not what we want.

This means that when we find a method in the method dictionary, we must first give it the actual receiver as argument. How are we going to obtain that receiver? well, the only possiblity is to parameterize objects by the current receiver they have to use when applying methods. That is to say, the value returned by the object construction form is not a "λ (msg . vals) ...." anymore, but a "λ (rcvr) ....". This effectively parameterizes our objects by "the current receiver". Similarly, if a message is not understood by a given object, then it must send the current receiver along to its parent.

This leaves us with one final question to address: how do we send a message to an object in the first place? Remember that our definition of is:
(defmac ( o m arg ...)
  (o 'm arg ...))
But now we cannot apply o as a function that takes a symbol (the message) and a variable number of arguments. Indeed, an object now is a function of the form (λ (rcvr) (λ (msg . args) ....)). So before we can pass the message and the arguments, we have to specify which object is the current receiver. Well, it’s easy, because at the time we are sending a message, the current receiver should be... the object we are sending the message to!

Why is the let binding necessary?

(defmac ( o m arg ...)
  (let ([obj o])
    ((obj obj) 'm arg ...)))

Let us see delegation—that is, late binding of self—at work:

(define seller
 (OBJECT-DEL root ()
  ([method price (prod)
           (* (case prod
                [(1) ( self price1)]
                [(2) ( self price2)])
              ( self unit))]
   [method price1 () 100]
   [method price2 () 200]
   [method unit () 1])))
(define broker
 (OBJECT-DEL seller ()
  ([method unit () 2])))

 

> ( seller price 1)

100

> ( broker price 1)

200

4.3 Programming with Prototypes

Object-based languages with a delegation mechanism like the one we have introduced in this chapter are called prototype-based languages. Examples are Self, JavaScript, and AmbientTalk, among many others. What are these languages good at? How to program with prototypes?

4.3.1 Singleton and Exceptional Objects

Since objects can be created ex-nihilo (ie. out of an object literal expression like OBJECT-DEL), it is natural to create one-of-a-kind objects. As opposed to class-based languages that require a specific design pattern for this (called Singleton), object-based languages are a natural fit for this case, as well as for creating "exceptional" objects (more on this below).

Let us first consider the object-oriented representation of booleans and a simple if-then-else control structure. How many booleans are there? well, two: true, and false. So we can create two standalone objects, true and false to represent them. In pure object-oriented languages like Self and Smalltalk, control structures like if-then-else, while, etc. are not primitives in the language. Rather, they are defined as methods on appropriate objects. Let us consider the if-then-else case. We can pass two thunks to a boolean, a truth thunk and a falsity thunk; if the boolean is true, it applies the truth thunk; if it is false, it applies the falsity thunk.

(define true
  (OBJECT-DEL root ()
    ([method ifTrueFalse (t f) (t)])))
 
(define false
  (OBJECT-DEL root ()
    ([method ifTrueFalse (t f) (f)])))

How can we use such objects? Look at the following example:

(define light
 (OBJECT-DEL root
   ([field on false])
   ([method turn-on () (set! on true)]
    [method turn-off () (set! on false)]
    [method on? () on])))

 

> ( ( light on?) ifTrueFalse (λ () "light is on")
                               (λ () "light is off"))

"light is off"

> ( light turn-on)
> ( ( light on?) ifTrueFalse (λ () "light is on")
                               (λ () "light is off"))

"light is on"

The objects true and false are the two unique representants of boolean values. Any conditional mechanism that relies on some expression being true or false can be similarly defined as methods of these objects. This is indeed a nice example of dynamic dispatch!

Boolean values and control structures in Smalltalk are defined similarly, but because Smalltalk is a class-based language, their definitions are more complex. Try it in your favorite class-based language.

Let us look at another example where object-based languages are practical: exceptional objects. First, recall the definition of typical point objects, which can be created using a factory function make-point:

(define (make-point x-init y-init)
  (OBJECT-DEL root
    ([field x x-init]
     [field y y-init])
    ([method x? () x]
     [method y? () y])))

Suppose we want to introduce an exceptional point object that has the particularity of having random coordinates, that change each time they are accessed. We can simply define this random-point as a standalone object whose x? and y? methods perform some computation, rather than accessing stored state:

(define random-point
  (OBJECT-DEL root ()
    ([method x? () (* 10 (random))]
     [method y? () ( self x?)])))
Note that random-point does not have any fields declared. Of course, because in OOP we rely on object interfaces, both representations of points can coexist.

4.3.2 Sharing through Delegation

The examples discussed above highlight the advantages of object-based languages. Let us now look at delegation in practice. First, delegation can be used to factor out shared behavior between objects. Consider the following:

(define (make-point x-init y-init)
  (OBJECT-DEL root
    ([field x x-init]
     [field y y-init])
    ([method x? () x]
     [method y? () y]
     [method above (p2)
             (if (> ( p2 y?) ( self y?))
                 p2
                 self)]
     [method add (p2)
             (make-point (+ ( self x?)
                            ( p2 x?))
                         (+ ( self y?)
                            ( p2 y?)))])))

All created point objects have the same methods, so this behavior could be shared by moving it to a common parent of all point objects (often called a prototype). Should all behavior be moved in the prototype? well, not if we want to allow different representations of points, like the random point above (which does not have any field at all!).

Therefore, we can define a point prototype, which factors out the above and add methods, whose implementation is common to all points:

(define point
  (OBJECT-DEL root ()
    ([method above (p2)
             (if (> ( p2 y?) ( self y?))
                 p2
                 self)]
     [method add (p2)
             (make-point (+ ( self x?)
                            ( p2 x?))
                         (+ ( self y?)
                            ( p2 y?)))])))

The required accessor methods could be declared as abstract methods in point, if our language supported such a concept. In Smalltalk, one would define the methods in point such that they throw an exception if invoked.

Note that as a standalone object, point does not make sense, because it sends messages to itself that it does not understand. But it can serve as a prototype from which different points can extend. Some are typical points, created with make-point, which hold two fields x and y:
(define (make-point x-init y-init)
  (OBJECT-DEL point
    ([field x x-init]
     [field y y-init])
    ([method x? () x]
     [method y? () y])))
While some can be exceptional points:
(define random-point
  (OBJECT-DEL point ()
    ([method x? () (* 10 (random))]
     [method y? () ( self x?)])))
As we said, these different kinds of point can cooperate, and they all understand the messages defined in the point prototype:
> (define p1 (make-point 1 2))
> (define p2 ( random-point add p1))
> ( ( p2 above p1) x?)

8.462365651077604

We can similarly use delegation to share state between objects. For instance, consider a family of points that share the same x-coordinate:

(define 1D-point
  (OBJECT-DEL point
    ([field x 5])
    ([method x? () x]
     [method x! (nx) (set! x nx)])))
 
(define (make-point-shared y-init)
  (OBJECT-DEL 1D-point
    ([field y y-init])
    ([method y? () y]
     [method y! (ny) (set! y ny)])))

All objects created by make-point-shared share the same parent, 1D-point, which determines their x-coordinate. If a change to 1D-point is made, it is naturally reflected in all its children:

> (define p1 (make-point-shared 2))
> (define p2 (make-point-shared 4))
> ( p1 x?)

5

> ( p2 x?)

5

> ( 1D-point x! 10)
> ( p1 x?)

10

> ( p2 x?)

10

4.4 Late Binding of Self and Modularity

In the definition of the OBJECT-DEL syntactic abstraction, notice that we use, in the definition of message sending, the self-application pattern (obj obj). This is similar to the self application pattern we have seen to achieve recursive binding without mutation.

See the Why of Y.

This feature of OOP is also known as "open recursion": any sub-object can redefine the meaning of a method in one of its parents. Of course, this is a mechanism that favors extensibility, because it is possible to extend any aspect of an object without having to foresee that extension. On the other hand, open recursion also makes software more fragile, because it becomes extremely easy to extend an object in unforeseen, incorrect ways. Imagine scenarios where this can be problematic and think about possible alternative designs. To shed some more light on fragility, think about black-box composition of objects: taking two objects, developed independently, and then putting them in a delegation relation with each other. What issues can arise?

Think about what you know from mainstream languages like C++ and Java: what means do these two languages provide to address this tradeoff between extensibility and fragility?

4.5 Lexical Scope and Delegation

As we have seen previously, we can define nested objects in our system. It is interesting to examine the relation between lexical nesting and delegation. Consider the following example:
(define parent
 (OBJECT-DEL root ()
   ([method foo () 1])))
 
(define outer
 (OBJECT-DEL root
    ([field foo (λ () 2)])
    ([method foo () 3]
     [method get ()
             (OBJECT-DEL parent ()
                ([method get-foo1 () (foo)]
                 [method get-foo2 () ( self foo)]))])))
 
(define inner ( outer get))

 

> ( inner get-foo1)

2

> ( inner get-foo2)

1

As you can see, a free identifier is looked up in the lexical environment (see get-foo1), while an unknown message is looked up in the chain of delegation (see get-foo2). This is important to clarify, because Java programmers are used to the fact that this.foo() is the same as foo(). In various languages that combine lexical nesting and some form of delegation (like inheritance), this is not the case.

Other languages have different take on the question. Check out Newspeak for instance.

So what happens in Java? Try it! You will see that the inheritance chain shadows the lexical chain: when using foo() if a method can be found in a superclass, it is invoked; only if there is no method found, the lexical environment (i.e., the outer object) is used. Referring to the outer object is therefore very fragile. This is why Java also supports an additional form Outer.this to refer to the enclosing object. Of course, then, if the method is not found directly in the enclosing object’s class, it is then looked up in its superclass, rather than upper in the lexical chain.

4.6 Delegation Models

The delegation model we have implemented here is but one point in the design space of prototype-based languages. Study the documentation of Self, JavaScript, and AmbientTalk to understand their designs. You can even modify our object system to support a different model, such as the JavaScript model.

4.7 Cloning

In our language, as in JavaScript, the way to create objects is literally: either we create an object from scratch, or we have a function whose role is to perform these object creations for us. Historically, prototype-based languages (like Self) have provided another way to create objects: by cloning existing objects. This approach emulates the copy-paste-modify metaphor we use so often with text (including code!): start from an object that is almost as you need, clone it, and modify the clone (eg. add a method, change a field).

When cloning objects in presence of delegation, the question of course arises of whether the cloning operation should be deep or shallow. Shallow cloning returns a new object that delegates to the same parent as the original object. Deep cloning returns a new object that delegates to a clone of the original parent, and so on: the whole delegation chain is cloned.

We won’t study cloning in more details in this course. You should however wonder how easy it would be to support cloning in our language. Since objects are in fact compiled into procedures (through macro expansion), the question boils down to cloning closures. Unfortunately, Scheme does not support such an operation. This is a case where the mismatch between the source and target languages shows up (recall Chapter 11 of PLAI). Nothing is perfect!