Next

Prev

Prev-tail

Tail

Up

Chapter 10
Objective Programming, urbiscript Object Model

This section presents object programing in urbiscript: the prototype-based object model of urbiscript, and how to define and use classes.

 10.1 Prototype-based programing in urbiscript
 10.2 Prototypes and slot lookup
 10.3 Copy on write
 10.4 Defining pseudo-classes
 10.5 Constructors
 10.6 Operators
 10.7 Properties
  10.7.1 Features of Values
  10.7.2 Features of Slots

10.1 Prototype-based programing in urbiscript

You’re probably already familiar with class-based object programing, since this is the C ++ model. Classes and objects are very different entities. Types are static entities that do not exist at run-time, while objects are dynamic entities that do not exist at compile time.

Prototype-based object programing is different: the difference between classes and objects, between types and values, is blurred. Instead, you have an object, that is already an instance, and that you might clone to obtain a new one that you can modify afterward. Prototype-based programming was introduced by the Self language, and is used in several popular script languages such as Io or JavaScript.

Class-based programming can be considered with an industrial metaphor: classes are molds, from which objects are generated. Prototype-based programming is more biological: a prototype object is cloned into another object which can be modified during its lifetime.

Consider pairs for instance (see Pair (Section 21.42)). Pairs hold two values, first and second, like an std::pair in C ++. Since urbiscript is prototype-based, there is no pair class. Instead, Pair is really a pair (object).

 
Pair; 
[00000000] (nil, nil)  

We can see here that Pair is a pair whose two values are equal to nil — which is a reasonable default value. To get a pair of our own, we simply clone Pair. We can then use it as a regular pair.

 
var p = Pair.clone; 
[00000000] (nil, nil) 
p.first = "101010"; 
[00000000] "101010" 
p.second = true; 
[00000000] true 
p; 
[00000000] ("101010", true) 
Pair; 
[00000000] (nil, nil)  

Since Pair is a regular pair object, you can modify and use it at will. Yet this is not a good idea, since you will alter your base prototype, which alters any derivative, future and even past.

 
var before = Pair.clone; 
[00000000] (nil, nil) 
Pair.first = false; 
[00000000] false 
var after = Pair.clone; 
[00000000] (false, nil) 
before; 
[00000000] (false, nil) 
// before and after share the same first: that of Pair. 
assert(Pair.first === before.first); 
assert(Pair.first === after.first);  

10.2 Prototypes and slot lookup

In prototype-based language, is-a relations (being an instance of some type) and inheritance relations (extending another type) are simplified in a single relation: prototyping. You can inspect an object prototypes with the protos method.

 
var p = Pair.clone; 
[00000000] (nil, nil) 
p.protos; 
[00000000] [(nil, nil)]  

As expected, our fresh pair has one prototype, (nil, nil), which is how Pair displays itself. We can check this as presented below.

 
// List.head returns the first element. 
p.protos.head; 
[00000000] (nil, nil) 
// Check that the prototype is really Pair. 
p.protos.head === Pair; 
[00000000] true  

Prototypes are the base of the slot lookup mechanism. Slot lookup is the action of finding an object slot when the dot notation is used. So far, when we typed obj.slot, slot was always a slot of obj. Yet, this call can be valid even if obj has no slot slot, because slots are also looked up in prototypes. For instance, p, our clone of Pair, has no first or second slots. Yet, p.first and p.second work, because these slots are present in Pair, which is p’s prototype. This is illustrated below.

 
var p = Pair.clone; 
[00000000] (nil, nil) 
// p has no slots of its own. 
p.localSlotNames; 
[00000000] [] 
// Yet this works. 
p.first; 
// This is because p has Pair for prototype, and Pair has a first slot. 
p.protos.head === Pair; 
[00000000] true 
"first" in Pair.localSlotNames && "second" in Pair.localSlotNames; 
[00000000] true  

As shown here, the clone method simply creates an empty object, with its target as prototype. The new object has the exact same behavior as the cloned on thanks to slot lookup.

Let’s experience slot lookup by ourselves. In urbiscript, you can add and remove prototypes from an object thanks to addProto and removeProto.

 
// We create a fresh object. 
var c = Object.clone; 
[00000000] Object_0x1 
// As expected, it has no slot slot. 
c.slot; 
[00000000:error] !!! lookup failed: slot 
var p = Object.clone; 
[00000000] Object_0x2 
var p.slot = 0; 
[00000000] 0 
c.addProto(p); 
[00000000] Object_0x1 
// Now, slot is found in c, because it is inherited from p. 
c.slot; 
[00000000] 0 
c.removeProto(p); 
[00000000] Object_0x1 
// Back to our good old lookup error. 
c.slot; 
[00000000:error] !!! lookup failed: slot  

The slot lookup algorithm in urbiscript in a depth-first traversal of the object prototypes tree. Formally, when the s slot is requested from x:

Thus, slots from the last prototype added take precedence over other prototype’s slots.

 
var proto1 = Object.clone; 
[00000000] Object_0x10000000 
var proto2 = Object.clone; 
[00000000] Object_0x20000000 
var o = Object.clone; 
[00000000] Object_0x30000000 
o.addProto(proto1); 
[00000000] Object_0x30000000 
o.addProto(proto2); 
[00000000] Object_0x30000000 
// We give o an x slot through proto1. 
var proto1.x = 0; 
[00000000] 0 
o.x; 
[00000000] 0 
// proto2 is visited first during lookup. 
// Thus its "x" slot takes precedence over proto1s. 
var proto2.x = 1; 
[00000000] 1 
o.x; 
[00000000] 1 
// Of course, os own slots have the highest precedence. 
var o.x = 2; 
[00000000] 2 
o.x; 
[00000000] 2  

You can check where in the prototype hierarchy a slot is found with the locateSlot method. This is a very handful tool when inspecting an object.

 
var p = Pair.clone; 
[00000000] (nil, nil) 
// Check that the first slot is found in Pair 
p.locateSlot("first") === Pair; 
[00000000] true 
// Where does locateSlot itself come from? Object itself! 
p.locateSlot("locateSlot"); 
[00000000] Object  

The prototype model is rather simple: creating a fresh object simply consists in cloning a model object, a prototype, that was provided to you. Moreover, you can add behavior to an object at any time with a simple addProto: you can make any object a fully functional Pair with a simple myObj.addProto(Pair).

10.3 Copy on write

One point might be bothering you though: what if you want to update a slot value in a clone of your prototype?

Say we implement a simple prototype, with an x slot equal to 0, and clone it twice. We have three objects with an x slot, yet only one actual 0 integer. Will modifying x in one of the clone change the prototype’s x, thus altering the prototype and the other clone as well?

The answer is, of course, no, as illustrated below.

 
var proto = Object.clone; 
[00000000] Object_0x1 
var proto.x = 0; 
[00000000] 0 
var o1 = proto.clone; 
[00000000] Object_0x2 
var o2 = proto.clone; 
[00000000] Object_0x3 
// Are we modifying protos x slot here? 
o1.x = 1; 
[00000000] 1 
// Obviously not 
o2.x; 
[00000000] 0 
proto.x; 
[00000000] 0 
o1.x; 
[00000000] 1  

This work thanks to copy-on-write: slots are first duplicated to the local object when they’re updated, as we can check below.

 
// This is the continuation of previous example. 
 
// As expected, o2 finds "x" in proto 
o2.locateSlot("x") === proto; 
[00000000] true 
// Yet o1 doesnt anymore 
o1.locateSlot("x") === proto; 
[00000000] false 
// Because the slot was duplicated locally 
o1.locateSlot("x") === o1; 
[00000000] true  

This is why, when we cloned Pair earlier, and modified the “first” slot of our fresh Pair, we didn’t alter Pair one all its other clones.

10.4 Defining pseudo-classes

Now that we know the internals of urbiscript’s object model, we can start defining our own classes.

But wait, we just said there are no classes in prototype-based object-oriented languages! That is true: there are no classes in the sense of C ++: compile-time entities that are not objects. Instead, prototype-based languages rely on the existence of a canonical object (the prototype) from which (pseudo) instances are derived. Yet, since the syntactic inspiration for urbiscript comes from languages such as Java, C ++ and so forth, it is nevertheless the class keyword that is used to define the pseudo-classes, i.e., prototypes.

As an example, we define our own Pair class. We just have to create a pair, with its first and second slots. For this we use the do scope described in Section 8.5. The listing below defines a new Pair class. The asString function is simply used to customize pairs printing — don’t give it too much attention for now.

 
var MyPair = Object.clone; 
[00000000] Object_0x1 
do (MyPair) 
{ 
  var first = nil; 
  var second = nil; 
  function asString () 
  { 
    "MyPair: " + first + ", " + second 
  }; 
} | {}; 
// We just defined a pair 
MyPair; 
[00000000] MyPair: nil, nil 
// Lets try it out 
var p = MyPair.clone; 
[00000000] MyPair: nil, nil 
p.first = 0; 
[00000000] 0 
p; 
[00000000] MyPair: 0, nil 
MyPair; 
[00000000] MyPair: nil, nil  

That’s it, we defined a pair that can be cloned at will! urbiscript provides a shorthand to define classes as we did above: the class keyword.

 
class MyPair 
{ 
  var first = nil; 
  var second = nil; 
  function asString() { "(" + first + ", " + second + ")"; }; 
}; 
[00000000] (nil, nil)  

The class keyword simply creates MyPair with Object.clone, and provides you with a do (MyPair) scope. It actually also pre-defines a few slots, but this is not the point here.

It is also possible to specify a proto for the newly created “class”, using the same syntax as Java and C ++:

 
class Top 
{ 
  var top = "top"; 
}; 
[00000000] Top 
 
class Bottom : Top 
{ 
  var bottom = "bottom"; 
}; 
[00000000] Bottom 
 
Bottom.new.top; 
[00000000] "top"  

For more details, see Section 20.1.6.8.

10.5 Constructors

As we’ve seen, we can use the clone method on any object to obtain an identical object. Yet, some classes provide more elaborate constructors, accessible by calling new instead of clone, potentially passing arguments.

 
var p = Pair.new("foo", false); 
[00000000] ("foo", false)  

While clone guarantees you obtain an empty fresh object inheriting from the prototype, new behavior is left to the discretion of the cloned prototype — although its behavior is the same as clone by default.

To define such constructors, prototypes only need to provide an init method, that will be called with the arguments given to new. For instance, we can improve our previous Pair class with a constructor.

 
class MyPair 
{ 
  var first = nil; 
  var second = nil; 
  function init(f, s) { first = f;   second = s;  }; 
  function asString() { "(" + first + ", " + second + ")"; }; 
}; 
[00000000] (nil, nil) 
MyPair.new(0, 1); 
[00000000] (0, 1)  

10.6 Operators

In urbiscript, operators such as +, && and others, are regular function that benefit from a bit of syntactic sugar. To be more precise, a+b is exactly the same as a.’+’(b). The rules to resolve slot names apply too, i.e., the ’+’ slot is looked for in a, then in its prototypes.

The following example provides arithmetic between pairs.

 
class ArithPair 
{ 
  var first = nil; 
  var second = nil; 
  function init(f, s) { first = f;   second = s;  }; 
  function asString() { "(" + first + ", " + second + ")"; }; 
  function ’+’(rhs) { new(first + rhs.first, second + rhs.second); }; 
  function ’-’(rhs) { new(first - rhs.first, second - rhs.second); }; 
  function ’*’(rhs) { new(first * rhs.first, second * rhs.second); }; 
  function ’/’(rhs) { new(first / rhs.first, second / rhs.second); }; 
}; 
[00000000] (nil, nil) 
ArithPair.new(1, 10) + ArithPair.new(2, 20) * ArithPair.new(3, 30); 
[00000000] (7, 610)  

10.7 Properties

Slots should be understood as names to values. Several names may point to the same value (a phenomenon called aliasing). Enriching values is easy: provide them with new slots. Yet it is sometimes needed to define the behavior of the slot rather than the property of the value. This is the purpose of properties in urbiscript.

10.7.1 Features of Values

In following example, we attach some random property foo to the value pointed to by the slot x.

 
var x = 123; 
[00000000] 123 
var x.foo = 42; 
[00000000] 42  

If y is another slot to the value of x, then it provides the same foo feature:

 
var y = x; 
[00000000] 123 
y.foo; 
[00000000] 42  

If x is bound to a new object (e.g., 456), then the feature foo is no longer present, since it’s a feature of the value (i.e., 123), and not one of the slot (i.e., x).

 
x = 456; 
[00000000] 456 
x.foo; 
[00000000:error] !!! lookup failed: foo  

Of course, y, which is still linked to the original value (123), answers to queries to foo.

 
y.foo; 
[00000000] 42  

10.7.2 Features of Slots

If, on the contrary you want to attach a feature to the slot-as-a-name, rather than to the value it contains, use the properties. The syntax is slotName->propertyName.

 
x = 123; 
[00000000] 123 
x->foo = 42; 
[00000000] 42 
x->foo; 
[00000000] 42  

Copying the value contained by a slot does not propagate the properties of the slot:

 
y = x; 
[00000000] 123 
y->foo; 
[00000000:error] !!! property lookup failed: y->foo  

And if you assign a new value to a slot, the properties of the slot are preserved:

 
x = 456; 
[00000000] 456 
x->foo = 42; 
[00000000] 42