this
– Simulating an Object Method
The only way for a script-defined function to change an external value is via this
.
Arguments passed to script-defined functions are always by value because functions are pure.
However, functions can also be called in method-call style:
object
.
method(
parameters …)
When a function is called this way, the keyword this
binds to the object in the
method call and can be changed.
fn change() { // note that the method does not need a parameter
this = 42; // 'this' binds to the object in method-call
}
let x = 500;
x.change(); // call 'change' in method-call style, 'this' binds to 'x'
x == 42; // 'x' is changed!
change(); // <- error: 'this' is unbound
Elvis Operator
The Elvis operator can be used to short-circuit
the method call when the object itself is ()
.
object
?.
method(
parameters …)
In the above, the method is never called if object is ()
.
Restrict the Type of this
in Function Definitions
Methods defined this way are automatically exposed to the global namespace.
In many cases it may be desirable to implement methods for different custom types using script-defined functions.
The Problem
Doing so is brittle and requires a lot of type checking code because there can only be one function definition for the same name and arity:
// Really painful way to define a method called 'do_update' on various data types
fn do_update(x) {
switch type_of(this) {
"i64" => this *= x,
"string" => this.len += x,
"bool" if this => this *= x,
"bool" => this *= 42,
"MyType" => this.update(x),
"Strange-Type#Name::with_!@#symbols" => this.update(x),
_ => throw `I don't know how to handle ${type_of(this)}`!`
}
}
The Solution
With a special syntax, it is possible to restrict a function to be callable only
when the object pointed to by this
is of a certain type:
fn
type name.
method(
parameters …) {
…}
or in quotes if the type name is not a valid identifier itself:
fn
"
type name string"
.
method(
parameters …) {
…}
int
can be used in place of the system integer type (usually i64
or i32
).
float
can be used in place of the system floating-point type (usually f64
or f32
).
Using these make scripts more portable.
Examples
/// This 'do_update' can only be called on objects of type 'MyType' in method style
fn MyType.do_update(x, y) {
this.update(x * y);
}
/// This 'do_update' can only be called on objects of type 'Strange-Type#Name::with_!@#symbols'
/// (which can be specified via 'Engine::register_type_with_name') in method style
fn "Strange-Type#Name::with_!@#symbols".do_update(x, y) {
this.update(x * y);
}
/// Define a blanket version
fn do_update(x, y) {
this = `${this}, ${x}, ${y}`;
}
/// This 'do_update' can only be called on integers in method style
fn int.do_update(x, y) {
this += x * y
}
let obj = create_my_type(); // 'x' is 'MyType'
obj.type_of() == "MyType";
obj.do_update(42, 123); // ok!
let x = 42; // 'x' is an integer
x.type_of() == "i64";
x.do_update(42, 123); // ok!
let x = true; // 'x' is a boolean
x.type_of() == "bool";
x.do_update(42, 123); // <- this works because there is a blanket version
// Use 'is_def_fn' with three parameters to test for typed methods
is_def_fn("MyType", "do_update", 2) == true;
is_def_fn("int", "do_update", 2) == true;
Bind to this
for Module Functions
The Problem
The method-call syntax is not possible for functions imported from modules.
import "my_module" as foo;
let x = 42;
x.foo::change_value(1); // <- syntax error
The Solution
In order to call a module function as a method, it must be
defined with a restriction on the type of object pointed to by this
:
┌────────────────┐
│ my_module.rhai │
└────────────────┘
// This is a typed method function requiring 'this' to be an integer.
// Typed methods are automatically marked global when importing this module.
fn int.change_value(offset) {
// 'this' is guaranteed to be an integer
this += offset;
}
┌───────────┐
│ main.rhai │
└───────────┘
import "my_module";
let x = 42;
x.change_value(1); // ok!
x == 43;