Mutable Global State
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:
-
It is a sure-fire way to create race conditions – that is why Rust does not support it;
-
It adds considerably to debug complexity – it is difficult to reason, in large code bases, where/when a state value is being modified;
-
It forces hard (but obscure) dependencies between separate pieces of code that are difficult to break when the need arises;
-
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
}