This section presents object programming in urbiscript: the prototype-based object model of urbiscript, and how to define and use classes.
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).
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);
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.
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).
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.
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.
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.
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)
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)
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.
In following example, we attach some random slot foo to the value pointed to by the slot x.
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).
Of course, y, which is still linked to the original value (123), answers to queries to foo.
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.
Copying the value contained by a slot does not propagate the properties of the slot:
And if you assign a new value to a slot, the properties of the slot are preserved:
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.
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: