Next

Prev

Prev-tail

Tail

Up

Chapter 9
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.

 9.1 Prototype-based programing in urbiscript
 9.2 Prototypes and slot lookup
 9.3 Copy on write
 9.4 Defining pseudo-classes
 9.5 Constructors
 9.6 Operators

9.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 20.40)). 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);  

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

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

9.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 7.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 MyPairObject.clone—, and provides you with a do (MyPair) scope. It actually also pre-defines a few slots, but this is not the point here.

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

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