CSE 341 -- Prototype-based Languages

In reaction to the complexity of classes and especially metaclasses, a number of researchers investigated prototype-based languages. In the pure form of these languages, there are no classes, just objects. New objects are created by copying existing ones, or making children of parent objects.

Advantages of prototype-based systems:

Main disadvantage:

Simplest version of a prototype-based system:

Suppose that objects are completely self-contained, so that an object consists of state and behavior. One can send messages to an object asking it for information, asking it to change its state, or asking it to change its behavior. The only way to make a new object is to make a complete copy of an existing object, copying both state and behavior. Once the copy is made, there is no further relation between the original and the copy. (Creating new objects by copying eliminates the need for metaclasses, since creation and modification messages are sent to prototypes or other individuals rather than to classes.)

This is a clean model, and would be easy to teach about. It handles object creation, modification, and representation. What is missing? First, there is no notion of classification of kinds of objects, either by message protocol or by representation. Second, there is no way to update a whole group of objects in a similar manner at one time (the equivalent of such actions as adding new methods to a class in Smalltalk). These are both important, and so the model needs to be augmented to support classification and updating.

One approach to providing the missing capabilities (classification and updating) is using delegation. Henry Lieberman discussed this in a paper in OOPSLA 1986; the SELF programming language (OOPSLA 1987) is a well-known prototype-based language. (As an aside, ThingLab (1981) adopted an intermediate approach: the user of its graphical interface would simultaneously construct a class and a prototypical instance.)

An object X can delegate a message to another object Y. Y handles the message for X. However, references to self in the method being executed go back to the original object X. (Notice that as a result this is different from having X just send the same message to Y. In this latter case, references to self would refer to Y.)

In Henry Lieberman's original proposal, we might have a prototypical point object, with x and y fields, and also code for methods such as +. To make a new point p, we make a child of point. p would typically override the slots for x and y inherited from point.

Suppose we send the message + with the argument q to p. p would look in its method dictionary, and not find +. So it would delegate the message to its parent point. It would execute this code. When the code sends self the message x, however, lookup starts back in p and the correct x is found.

An odd consequence of Lieberman's model is that one can, for example, override the x field from the prototype point and continue to inherit the y field. If the x and y fields of point are changed, the child will see a change in its y but not in its x.

However, a benefit of this model is that any object can have its own special behavior (for example, p could have its own version of some method).

SELF also adopts the delegation model. As an organizing principle, though, an object such as point is divided into two objects: a prototype point with x and y slots, with a parent point traits. To make a new point, we clone the point object (i.e. make a copy of it - not a child). point traits serves a class-like role.

SELF tries to blur the distinction between instance variables and methods. Both state (such as x) and code (such as the method for +) are stored in slots. All access is via message sends. When an object receives a message, it looks for a slot of that name. If it finds one, it gets the contents of the slot, evaluates it, and returns the result. Otherwise it delegates the message to its parent(s). Most objects evaluate to themselves; however, code evalutes by running. Note that this makes it tricky to manipulate methods -- this is done in SELF using mirrors. Given a slot named foo, SELF automatically defines access and setting methods foo and foo:.