How To Override Operators In Classes And Tables
Metamethods allow Squirrel programmers to override existing language operators. Essentially each metamethod provides a means by which Squirrel can substitute your functionality for its own, standard operations whenever it encounters one of a specific set of operators.
This is best explained with an example. When Squirrel encounters any one of the relative comparison operators — <=
, >=
, <
and >
— to the right of an object, it immediately looks in that object’s specified delegate (if it has one) for the _cmp metamethod. If Squirrel finds a function named _cmp(), it will call that function, otherwise it runs code of its own. There is a full list of available metamethods at the end of this guide.
An instance’s delegate is always its Squirrel type or custom class. If metamethods are therefore defined within the class declaration. Tables have no delegate by default, but you can set one (always another table) using its setdelegate() method. The table’s metamethods are set in the delegate table:
// Classes
class C {
value = null;
constructor(h, w) {
value = { "height": h, "width": w};
}
// Class definition contains the metamethod(s) used by its instances
function _cmp(x) {
local a_this = value.height * value.width;
local a_x = x.value.height * x.value.width;
if (a_this > a_x) return 1;
if (a_this == a_x) return 0;
return -1;
}
}
// Instance has access to metamethod(s) defined in class
local c = C(640, 480);
local d = C(1024, 768);
if (c >= d) server.log("'c' is greater than or equal to 'd'");
if (c < d) server.log("'c' is less than 'd'");
// Tables
tableDelegate <- {
// Table delegate contains the metamethods used by bound tables
function _cmp(x) {
if ("height" in this && "height" in x && this.height > x.height) return 1;
if ("height" in this && "height" in x && this.height == x.height) return 0;
return -1;
}
}
// New tables do not have access to metamethods...
local t1 = { "height": 480, "width": 640};
local t2 = { "height": 768, "width": 1024};
// ...until their delegate is set to the table containing the metamethod declarations
t1.setdelegate(tableDelegate);
// NOTE 't2' does not need to have its delegate set (ie. gain access to the metamethod)
// unless it is placed on the left side of a relative comparison
server.log(t1 > t2 ? "'t1' is taller than 't2'" : "'t2' is taller than 't1'");
The code below provides an example of the use of another metamethod, _newslot, which is triggered by the <-
operator:
function _newslot(key, value) {
// _newslot metamethod must take two parameters
server.log("Slot '" + key + "' added (value: " + value + ")");
}
local prefs = {};
prefs.setdelegate(this);
prefs.on <- true;
prefs.red <- 0;
prefs.green <- 0;
prefs.blue <- 255;
prefs.brightness <- 100;
Run this code and you will see the following in the log:
2020-07-16T09:16:19.890Z [agent.log] Slot 'on' added (value: true)
2020-07-16T09:16:19.890Z [agent.log] Slot 'red' added (value: 0)
2020-07-16T09:16:19.890Z [agent.log] Slot 'green' added (value: 0)
2020-07-16T09:16:19.890Z [agent.log] Slot 'blue' added (value: 255)
2020-07-16T09:16:19.890Z [agent.log] Slot 'brightness' added (value: 100)
For simplicity, we have defined _newslot() in Squirrel’s root table, referenced here by this. Tables have no delegate by default, so we need to explicitly add one to prefs using the setdelegate() method. Your metamethod definitions for operators performed on tables are always entered into this second, delegate table. Your delegate need not be the root table, but can instead be — and more commonly is — a specially created table:
local delegateTable = {
function _newslot(key, value) {
server.log("Slot '" + key + "' added (value: " + value + ")");
}
}
local prefs = {};
prefs.setdelegate(delegateTable);
prefs.on <- true;
prefs.red <- 0;
prefs.green <- 0;
prefs.blue <- 255;
prefs.brightness <- 100;
The _newslot() function only works for prefs; slots created in other tables will be created as normal, unless they too have had delegateTable (or another table containing _newslot()) set as their delegate.
Metamethods only work with certain entities — tables, class objects and class instances — and a given metamethod will not necessarily work with all of those entities. The metamethod _newslot, used above, is only relevant to tables, not to class instances or class objects.
For instances of Squirrel classes, the approach is slightly different: the ‘delegate’ is always the class itself. All instances of the same class therefore share the same delegate, and you declare your metamethods in the class definition. You can’t call setdelegate() on an instance, and you can’t use a class as the delegate of a table.
You may have noticed that _newslot() doesn’t actually create the slot; Squirrel relies on it to do so, and if you check a slot value without creating the slot, you’ll get the usual ‘index does not exist’ error. All we have to do to prevent this is add a line to _newslot():
function _newslot(key, value) {
server.log("Slot '" + key + "' added (value: " + value + ")");
this[key] <- value;
}
this is the hidden parameter passed into all function calls that points to the object making the call — in this case, the table prefs. We put key in square brackets because this is Squirrel syntax for creating a new slot whose name is the value of key, ie. not the string "key"
itself.
If you’re also interested in the removal of slots from a table, you can use the _delslot metamethod to override Squirrel’s delete
keyword.
Squirrel also provides two other metamethods, _set and _get, which can be used when working with tables (and class instances). Remember, everything in Squirrel is, at heart, a table. _get is called when your delegated table attempts to get a value from a key that doesn’t exist. Instead of simply reporting the usual ‘index does not exist’ runtime error, Squirrel gives you a chance to override that behavior:
function _get(key) {
// The delegated table has no key 'key' so
// report it, but don't cause a runtime error
server.error("Index '" + key + "' doesn’t exist");
}
_set works the same way, but for cases where you are attempting to set a value to a non-existent slot:
function _set(key, value) {
// The delegated table has no key 'key' so
// report it, but don't cause a runtime error
server.error("Index '" + key + "' doesn’t exist");
}
If you run the above examples in agent code, you may get the error “halting stuck metamethod”. This arises because Squirrel agent code is time-sliced with all other Squirrel agent code. For technical reasons, Squirrel can’t be time-sliced while executing a metamethod. So each metamethod call is limited to a single time-slice (1000 Squirrel compiled bytecode instructions) before it is aborted as a potential CPU hog, ie. potentially unfair to other agents running on the same system. There is no way around this other than to make the metamethod use fewer instructions.
Device-side Squirrel code, which can’t be ‘unfair’ to anyone except itself and so is not time-sliced, does not have this restriction.
So what, then, are metamethods actually for? Why would you use them?
Essentially, metamethods provide you with a means to create new data types which require operations not supported by standard Squirrel.
For example, let’s say your application works with points in three-dimensional space. You might create a class to represent each such point:
class Point {
x = 0;
y = 0;
z = 0;
constructor (...) {
// NOTE the parameter ... passes all arguments as the array 'vargv'
if (vargv.len() != 3) throw "Three co-ordinates (x,y,z), please";
x = vargv[0];
y = vargv[1];
z = vargv[2];
}
}
If we have two Point instances and we wish to multiply them, the following code will produce the wrong result:
local p1 = Point(1,2,3);
local p2 = Point(5,6,7);
local p3 = p1 * p2;
This is because p1 and p2 are references to the two instances of Point, so p3 will be the product of those references. In fact, this is an operation that Squirrel doesn’t allow — try the code above and you’ll get an “arith op * on between ‘instance’ and ‘instance’” error. Clearly, this isn’t what we want: we expect p3 to be an instance of Point generated by multiplying the two other co-ordinate sets (ie. two 3 x 1 matrices). So we need to override the *
operator. To do this, we use the metamethod _mul, which we add to the Point class definition:
function _mul(p) {
return Point(x * p.x, y * p.y, z * p.z);
}
This time when Squirrel reaches the *
operator following a Point instance, it checks for and finds _mul() in p1 and uses that method in place of its usual multiplication routine, by passing p2 into p1’s _mul() method. We can confirm that by adding:
server.log("("+ p3.x + "," + p3.y + "," + p3.z + ")");
which logs:
2015-11-20 12:46:47 [agent.log] (5,12,21)
Squirrel provides metamethods for other arithmetic functions: _add, _sub, _div, _mod (modulo, the % operator) and _unm (unary minus, the operator which negates a value).
It’s important to understand that the order of the elements in the statement is important. Squirrel checks for metamethods only on the operand on the left of the operator. For example, you might want to multiply a Point by an integer. Your code can check the value passed into your _mul() code and perform the calculation accordingly. However, you must write such a sum in the following order:
local p3 = p1 * 5;
If you were to write the (ordinarily) mathematically equivalent:
local p3 = 5 * p1;
then Squirrel will throw an error: it sees the first term, the 5, and fixes itself to perform integer math. The Point instance, p1, isn’t an integer, and hence the error message. Even if p1 has a _mul metamethod, it will not be called. p1 must come first. This runs contrary to how a number of other languages handle operator overrides.
There are other metamethods too. For example, if we later want to check whether a poorly named variable, a, is an instance of Point, we can add the following code to the class to make use of the _typeof metamethod:
function _typeof() {
return "Point";
}
then if we add the following line to the body of our program:
server.log(typeof p3);
we will see:
2015-11-20 12:46:47 [agent.log] Point
A more practical use of this is in the class’ _mul metamethod to make sure that the passed parameter is valid:
function _mul(p2) {
if (typeof p2 != "Point") throw "Operand is not a Point object";
return Vector(x * p2.x, y * p2.y, z * p2.z);
}
Other useful metamethods include _tostring, which returns a string representation of the table or instances’s value whenever you call its .tostring() delegate method. For example:
function _tostring() {
return (x + "," + y + "," + z);
}
will ensure that the code server.log(p3);
prints out something useful.
The _nexti metamethod is triggered when a foreach
structure loops. You might override this behavior too, say, iterate through every other item.
You can view an example of the use of _nexti in our mbstring library:
function _nexti(previdx) {
// _str is the internal UTF-8 string buffer:
// an array of 1-, 2-, 3- or 4-byte strings
if (_str.len() == 0) {
return null;
} else if (previdx == null) {
return 0;
} else if (previdx == _str.len() - 1) {
return null;
} else {
return previdx + 1;
}
}
This code is called as users of the library use a foreach
loop to iterate through the components of the UTF-8 string stored by an instance of the library class. For example:
#require "mbstring.class.nut:1.0.0"
local utf8String = mbstring("123«€àâäèéêëîïôœùûüÿçÀÂÄÈÉÊËÎÏÔŒÙÛÜŸ»")
foreach (index, character in utf8String) {
server.log("Character: " + index + ": " + character);
}
Each time through the loop, Squirrel runs the mbstring’s _nexti() method to determine what the next value of the loop index (here placed in index) will be. In fact, the mbstring library, whose source code you can view in Electric Imp’s public GitHub repo provides a good example of the usage of many of the metamethods discussed above.
You should note, though, that _nexti() only works with class instances — it can’t be used to control an iteration working through a table’s keys, for example.
Both _inherited and _newmember are metamethods that allow you to override what takes place when, respectively, a parent class is overridden by an child class, and a class member entity is declared. _cloned is called when an object is copied. As such these two metamethods are only relevant to class objects. Indeed, they are the only metamethods available to class objects.
Finally, _call can be used in order to call a table or a class instance itself as if it were a function. For example, the following code will fail with the error, ERROR: attempt to call 'instance'
:
// Define a class
class C {
function test() { server.log("test() called"); }
};
// Instance the class...
local c = C();
// ...and call the instance
c();
However, adding _call solves this, causing the correct output:
// Define a class
class C {
function _call(x) { test() };
function test() { server.log("test() called"); }
};
// Instance the class...
local c = C();
// ...and call the instance
c();
For a table, you set _call by adding the metamethod to a table and then setting that table is your primary table’s delegate, as we saw earlier:
// Define a delegate table
TD <- {
function _call(x) { test(); },
function test() { server.log("test() called") }
};
// Create a primary table and set its delegate
local myTable = {};
myTable.setdelegate(TD);
// ...and call the primary table
myTable();
Metamethod | Operator(s) | Parameter(s) | Use With | Notes |
---|---|---|---|---|
_cmp | <, >, <=, >= | Operand to the right of the operator | Table, class instance | Perform a relative comparison, eg. if (a > b) { ... } Function should return an integer: 1, if a > b 0, if a == b -1, if a < b |
_mul | * | Operand to the right of the operator | Table, class instance | Perform a multiplication, eg. local a = b * c Returns the result |
_add | + | Operand to the right of the operator | Table, class instance | Perform an addition, eg. local a = b + c Returns the result |
_sub | - | Operand to the right of the operator | Table, class instance | Perform a subtraction, eg. local a = b - c Returns the result |
_div | / | Operand to the right of the operator | Table, class instance | Perform a division, eg. local a = b / c Returns the result |
_mod | % | Operand to the right of the operator | Table, class instance | Perform a modulo, eg. local a = b % c Returns the result |
_unm | - | Operand to the right of the operator | Table, class instance | Perform a unary minus, eg. local a = -b Returns the result |
_newslot | <- | Key and value | Table | Creates and adds a new slot to a table |
_delslot | delete | Key | Table | Removes a slot from a table |
_set | = | Key and value | Table, class instance | Called when code attempts to set a non-existent slot’s key, eg. table.a = b |
_get | = | Key | Table, class instance | Called when code attempts to get the value of a non-existent slot, eg. local a = table.b |
_typeof | typeof | None | Table, class instance | Returns type of object or class instance as a string, eg. local a = typeof b |
_tostring | .tostring() | None | Table, class instance | Returns the value of the object or class instance as a string, eg. local a = b.tostring() |
_nexti | foreach… in… | Previous iteration index | Class instance | Called at each iteration of a foreach loop. Parameter value will be null at the first iteration.Function must return the next index value |
_cloned | clone | The original instance or table | Table, class instance | Called when an instance or table is cloned |
_inherited | New class (as this) and its attributes | Class object | A parent class method is overridden by a child class | |
_newmember | index, value, attributes, isstatic | Class object | Called when a new class member is declared. If implemented, members will not be added to the class | |
_call | this and the function’s other (visible) parameters | Table, class instance | Called when the table or class instance is itself called as a function |