This repository has been archived on 2025-08-04. You can view files and clone it, but cannot push or open issues or pull requests.
rhaj/rhai_engine/rhaibook/patterns/global-mutable-state.md
2025-04-03 09:18:05 +02:00

4.2 KiB
Raw Blame History

Mutable Global State

{{#include ../links.md}}

Don't Do It™


Generations of programmers struggled to get around mutable global state (a.k.a. the `window` object)
in the design of JavaScript.

In contrast to global constants, mutable global states are strongly discouraged because:

  1. It is a sure-fire way to create race conditions that is why Rust does not support it;

  2. It adds considerably to debug complexity it is difficult to reason, in large code bases, where/when a state value is being modified;

  3. It forces hard (but obscure) dependencies between separate pieces of code that are difficult to break when the need arises;

  4. It is almost impossible to add new layers of redirection and/or abstraction afterwards without major surgery.

Alternative Use this

In the majority of the such scenarios, there is only one mutable global state of interest.

Therefore, it is a much better solution to bind that global state to the this pointer.

// Say this is a mutable global state...
let state = #{ counter: 0 };

// This function tries to access the global 'state'
// which will fail.
fn inc() {
    state.counter += 1;
}

// The function should be written with 'this'
fn inc() {
    this.counter += 1;
}

state.inc();        // call 'inc' with 'state' bound to 'this'

// Or this way... why hard-code the state in the first place?
fn inc() {
    this += 1;
}

state.counter.inc();

There are good reasons why using `this` is a better solution:

* the state is never _hidden_ – it is always clear to see what is being modified
* it is just as fast – the `this` pointer works by reference
* you can pass other states in, in the future, without changing the script code
* there are no hard links within functions that will be difficult to unravel
* only the [variable] bound to `this` is ever modified; everything else is immutable

This is not something that Rhai encourages.  _You Have Been Warned™_.

There are two ways...

Option 1 Get/Set Functions

This is similar to the Control Layer pattern.

Use get/set functions to read/write the global mutable state.

// The globally mutable shared value
let value = Rc::new(RefCell::new(42));

// Register an API to access the globally mutable shared value
let v = value.clone();
engine.register_fn("get_global_value", move || *v.borrow());

let v = value.clone();
engine.register_fn("set_global_value", move |value: i64| *v.borrow_mut() = value);

These functions can be used in script [functions] to access the shared global state.

fn foo() {
    let current = get_global_value();       // Get global state value
    current += 1;
    set_global_value(current);              // Modify global state value
}

This option is preferred because it is possible to modify the get/set functions later on to add/change functionalities without introducing breaking script changes.

Option 2 Variable Resolver

Declare a [variable resolver] that returns a shared value which is the global state.

// Use a shared value as the global state
let value: Dynamic = 1.into();
let mut value = value.into_shared();        // convert into shared value

// Clone the shared value
let v = value.clone();

// Register a variable resolver.
engine.on_var(move |name, _, _| {
    match name
        "value" => Ok(Some(v.clone())),
        _ => Ok(None)
    }
});

// The shared global state can be modified
*value.write_lock::<i64>().unwrap() = 42;

The global state variable can now be used just like a normal local variable, including modifications.

fn foo() {
    value = value * 2;
//          ^ global variable can be read
//  ^ global variable can also be modified
}

This option makes mutable global state so easy to implement that it should actually be
considered an _Anti-Pattern_.