IndexNextUpPreviousUrbi SDK 3.0.0

Chapter 7
Objective Programming, urbiscript Object Model

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

 7.1 Prototype-Based Programming in urbiscript
 7.2 Prototypes and Slot Lookup
 7.3 Copy on Write
 7.4 Defining Pseudo-Classes
 7.5 Constructors
 7.6 Operators
 7.7 Properties
  7.7.1 Features of Values
  7.7.2 Features of Slots
 7.8 Getters and setters
  7.8.1 When to use properties?

7.1 Prototype-Based Programming in urbiscript

You’re probably already familiar with class-based object programming, since this is the C++, Java, C# model. Classes and objects are very different entities. Classes and 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 programming 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). 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);  

7.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_0x00000001 
// As expected, it has no ’slot’ slot. 
c.slot; 
[00000000:error] !!! lookup failed: slot 
var p = Object.clone(); 
[00000000] Object_0x00000002 
var p.slot = 0; 
[00000000] 0 
c.addProto(p); 
[00000000] Object_0x00000001 
// Now, ’slot’ is found in c, because it is inherited from p. 
c.slot; 
[00000000] 0 
c.removeProto(p); 
[00000000] Object_0x00000001 
// 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 proto1’s. 
var proto2.x = 1; 
[00000000] 1 
o.x; 
[00000000] 1 
// Of course, o’s 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).

7.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_0x00000001 
var proto.x = 0; 
[00000000] 0 
var o1 = proto.clone(); 
[00000000] Object_0x00000002 
var o2 = proto.clone(); 
[00000000] Object_0x00000003 
// Are we modifying proto’s 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 doesn’t 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.

7.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++, i.e., 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 5.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_0x00000001 
do (MyPair) 

  var first = nil; 
  var second = nil; 
  function asString () 
  { 
    "MyPair: " + first + ", " + second 
  }; 
}|; 
// We just defined a pair 
MyPair; 
[00000000] MyPair: nil, nil 
// Let’s 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.

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

7.6 Operators

In urbiscript, operators such as +, && and others, are regular functions 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)  

7.7 Properties

Sometimes one needs to attach attributes to a variable, and not the value it contains, for instance to store whether the variable is constant or not. In urbiscript we use objects named Slots that stands before the actual value, and persist when a new value is assigned to the variable. Variables of the Slot are called properties.

7.7.1 Features of Values

In following example, we attach some random slot 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 
// The value in the slots x and y are the same object 
x===y; 
[00000000] true 
x.foo = 43 | y.foo; 
[00000001] 43  

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] 43  

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

7.8 Getters and setters

All the properties with a special meaning are described in Slot. Two of particular interest are oget and oset. They can be used to define a getter and a setter function, which are called each time the Slot is read or written to, respectively, thus implementing the property semantics as defined in JavaScript. urbiscript also borrowed the JavaScript shortcut syntax get foo and set foo to define setter and getter properties.

Consider this example:

 
class Vector 

  var x=0; 
  var y=0; 
  function init(x=0, y=0) 
  { 
    this.x = x; // Here we use copy on write to create our own slot x. 
    var this.y = y; // Here we force local slot creation, both are valid. 
  }; 
  // Return the L2 norm of the vector 
  get norm() // sugar for norm->oget = function() 
  { 
    (x*x+y*y).sqrt() 
  }; 
  // Scale the vector so that its norm becomes newNorm 
  set norm(newNorm) // sugar for norm->oset = function(newNorm) 
  { 
    var oldNorm = norm; 
    x *= newNorm/oldNorm; 
    y *= newNorm/oldNorm; 
  }; 
  function asString() 
  { 
    "<" + x + "," + y + ">" 
  }; 
}; 
[00000000] <0,0> 
var v = Vector.new(1, 1); 
[00016149] <1,1> 
v.norm; 
[00018036] 1.41421 
v.norm = 2.sqrt() | v; 
[00177142] <1,1>  

If you use a setter without a getter, the value returned by your function will be stored if it is not void, as demonstrated in Slot.set.

7.8.1 When to use properties?

The c# documentation provides a very good explanation of when to use properties that we took the liberty to reproduce verbatim here:

In most cases, properties represent data, and methods perform actions. Properties are accessed like fields, which makes them easier to use. If a method takes no arguments and returns an object’s state information, or accepts a single argument to set some part of an object’s state, it is a good candidate for becoming a property.

Properties should behave as if they are fields; if the method cannot, it should not be changed to a property. Methods are preferable to properties in the following situations: