195 lines
7.4 KiB
Markdown
195 lines
7.4 KiB
Markdown
Closures
|
|
========
|
|
|
|
{{#include ../links.md}}
|
|
|
|
~~~admonish tip.side "Tip: `is_shared`"
|
|
|
|
Use `Dynamic::is_shared` to check whether a particular [`Dynamic`] value is shared.
|
|
~~~
|
|
|
|
Although [anonymous functions] de-sugar to standard function definitions, they differ from standard
|
|
functions because they can _captures_ [variables] that are not defined within the current scope,
|
|
but are instead defined in an external scope – i.e. where the [anonymous function] is created.
|
|
|
|
All [variables] that are accessible during the time the [anonymous function] is created are
|
|
automatically captured when they are used, as long as they are not shadowed by local [variables]
|
|
defined within the function's.
|
|
|
|
The captured [variables] are automatically converted into **reference-counted shared values**
|
|
(`Rc<RefCell<Dynamic>>`, or `Arc<RwLock<Dynamic>>` under [`sync`]).
|
|
|
|
Therefore, similar to closures in many languages, these captured shared values persist through
|
|
reference counting, and may be read or modified even after the [variables] that hold them go out of
|
|
scope and no longer exist.
|
|
|
|
```admonish tip.small "Tip: Disable closures"
|
|
|
|
Capturing external [variables] can be turned off via the [`no_closure`] feature.
|
|
```
|
|
|
|
|
|
Examples
|
|
--------
|
|
|
|
```rust
|
|
let x = 1; // a normal variable
|
|
|
|
x.is_shared() == false;
|
|
|
|
let f = |y| x + y; // variable 'x' is auto-curried (captured) into 'f'
|
|
|
|
x.is_shared() == true; // 'x' is now a shared value!
|
|
|
|
f.call(2) == 3; // 1 + 2 == 3
|
|
|
|
x = 40; // changing 'x'...
|
|
|
|
f.call(2) == 42; // the value of 'x' is 40 because 'x' is shared
|
|
|
|
// The above de-sugars into something like this:
|
|
|
|
fn anon_0001(x, y) { x + y } // parameter 'x' is inserted
|
|
|
|
make_shared(x); // convert variable 'x' into a shared value
|
|
|
|
let f = anon_0001.curry(x); // shared 'x' is curried
|
|
```
|
|
|
|
|
|
~~~admonish bug "Beware: Captured Variables are Truly Shared"
|
|
|
|
The example below is a typical tutorial sample for many languages to illustrate the traps
|
|
that may accompany capturing external [variables] in closures.
|
|
|
|
It prints `9`, `9`, `9`, ... `9`, `9`, not `0`, `1`, `2`, ... `8`, `9`, because there is
|
|
ever only _one_ captured [variable], and all ten closures capture the _same_ [variable].
|
|
|
|
```rust
|
|
let list = [];
|
|
|
|
for i in 0..10 {
|
|
list.push(|| print(i)); // the for loop variable 'i' is captured
|
|
}
|
|
|
|
list.len() == 10; // 10 closures stored in the array
|
|
|
|
list[0].type_of() == "Fn"; // make sure these are closures
|
|
|
|
for f in list {
|
|
f.call(); // all references to 'i' are the same variable!
|
|
}
|
|
```
|
|
~~~
|
|
|
|
~~~admonish danger "Be Careful to Prevent Data Races"
|
|
|
|
Rust does not have data races, but that doesn't mean Rhai doesn't.
|
|
|
|
Avoid performing a method call on a captured shared [variable] (which essentially takes a
|
|
mutable reference to the shared object) while using that same [variable] as a parameter
|
|
in the method call – this is a sure-fire way to generate a data race error.
|
|
|
|
If a shared value is used as the `this` pointer in a method call to a closure function,
|
|
then the same shared value _must not_ be captured inside that function, or a data race
|
|
will occur and the script will terminate with an error.
|
|
|
|
```rust
|
|
let x = 20;
|
|
|
|
x.is_shared() == false; // 'x' is not shared, so no data race is possible
|
|
|
|
let f = |a| this += x + a; // 'x' is captured in this closure
|
|
|
|
x.is_shared() == true; // now 'x' is shared
|
|
|
|
x.call(f, 2); // <- error: data race detected on 'x'
|
|
```
|
|
~~~
|
|
|
|
~~~admonish danger "Data Races in `sync` Builds Can Become Deadlocks"
|
|
|
|
Under the [`sync`] feature, shared values are guarded with a `RwLock`, meaning that data race
|
|
conditions no longer raise an error.
|
|
|
|
Instead, they wait endlessly for the `RwLock` to be freed, and thus can become deadlocks.
|
|
|
|
On the other hand, since the same thread (i.e. the [`Engine`] thread) that is holding the lock
|
|
is attempting to read it again, this may also [panic](https://doc.rust-lang.org/std/sync/struct.RwLock.html#panics-1)
|
|
depending on the O/S.
|
|
|
|
```rust
|
|
let x = 20;
|
|
|
|
let f = |a| this += x + a; // 'x' is captured in this closure
|
|
|
|
// Under `sync`, the following may wait forever, or may panic,
|
|
// because 'x' is locked as the `this` pointer but also accessed
|
|
// via a captured shared value.
|
|
x.call(f, 2);
|
|
```
|
|
~~~
|
|
|
|
|
|
TL;DR
|
|
-----
|
|
|
|
```admonish question "How is it actually implemented?"
|
|
|
|
The actual implementation of closures de-sugars to:
|
|
|
|
1. Keeping track of what [variables] are accessed inside the [anonymous function],
|
|
|
|
2. If a [variable] is not defined within the [anonymous function's][anonymous function] scope,
|
|
it is looked up _outside_ the [function] and in the current execution scope –
|
|
where the [anonymous function] is created.
|
|
|
|
3. The [variable] is added to the parameters list of the [anonymous function], at the front.
|
|
|
|
4. The [variable] is then converted into a **reference-counted shared value**.
|
|
|
|
An [anonymous function] which captures an external [variable] is the only way to create a
|
|
reference-counted shared value in Rhai.
|
|
|
|
5. The shared value is then [curried][currying] into the [function pointer] itself,
|
|
essentially carrying a reference to that shared value and inserting it into future calls of the [function].
|
|
|
|
This process is called _Automatic Currying_, and is the mechanism through which Rhai simulates closures.
|
|
```
|
|
|
|
```admonish question "Why automatic currying?"
|
|
|
|
In concept, a closure _closes_ over captured variables from the outer scope – that's why
|
|
they are called _closures_. When this happen, a typical language implementation hoists
|
|
those variables that are captured away from the stack frame and into heap-allocated storage.
|
|
This is because those variables may be needed after the stack frame goes away.
|
|
|
|
These heap-allocated captured variables only go away when all the closures that need them
|
|
are finished with them. A garbage collector makes this trivial to implement – they are
|
|
automatically collected as soon as all closures needing them are destroyed.
|
|
|
|
In Rust, this can be done by reference counting instead, with the potential pitfall of creating
|
|
reference loops that will prevent those variables from being deallocated forever.
|
|
Rhai avoids this by clone-copying most data values, so reference loops are hard to create.
|
|
|
|
Rhai does the hoisting of captured variables into the heap by converting those values
|
|
into reference-counted locked values, also allocated on the heap. The process is identical.
|
|
|
|
Closures are usually implemented as a data structure containing two items:
|
|
|
|
1. A function pointer to the function body of the closure,
|
|
2. A data structure containing references to the captured shared variables on the heap.
|
|
|
|
Usually a language implementation passes the structure containing references to captured
|
|
shared variables into the function pointer, the function body taking this data structure
|
|
as an additional parameter.
|
|
|
|
This is essentially what Rhai does, except that Rhai passes each variable individually
|
|
as separate parameters to the function, instead of creating a structure and passing that
|
|
structure as a single parameter. This is the only difference.
|
|
|
|
Therefore, in most languages, essentially all closures are implemented as automatic currying of
|
|
shared variables hoisted into the heap, automatically passing those variables as parameters into
|
|
the function. Rhai just brings this directly up to the front.
|
|
```
|