this – Simulating an Object Method

Functions are pure

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

Tip: Automatically global

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) {}

Type name must be the same as type_of

The type name specified in front of the function name must match the output of type_of for the required type.

Tip: int and float

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;