reorganize module
This commit is contained in:
193
_archive/rhai_engine/rhaibook/rust/dynamic-args.md
Normal file
193
_archive/rhai_engine/rhaibook/rust/dynamic-args.md
Normal file
@@ -0,0 +1,193 @@
|
||||
`Dynamic` Parameters in Rust Functions
|
||||
======================================
|
||||
|
||||
{{#include ../links.md}}
|
||||
|
||||
It is possible for Rust functions to contain parameters of type [`Dynamic`].
|
||||
|
||||
A [`Dynamic`] value can hold any clonable type.
|
||||
|
||||
```admonish question.small "Trivia"
|
||||
|
||||
The `push` method of an [array] is implemented as follows (minus code for [safety] protection
|
||||
against [over-sized arrays][maximum size of arrays]), allowing the function to be called with
|
||||
all item types.
|
||||
|
||||
~~~rust
|
||||
// 'item: Dynamic' matches all data types
|
||||
fn push(array: &mut Array, item: Dynamic) {
|
||||
array.push(item);
|
||||
}
|
||||
~~~
|
||||
```
|
||||
|
||||
|
||||
Precedence
|
||||
----------
|
||||
|
||||
Any parameter in a registered Rust function with a specific type has higher precedence over
|
||||
[`Dynamic`], so it is important to understand which _version_ of a function will be used.
|
||||
|
||||
Parameter matching starts from the left to the right.
|
||||
Candidate functions will be matched in order of parameter types.
|
||||
|
||||
Therefore, always leave [`Dynamic`] parameters (up to 16, see below) as far to the right as possible.
|
||||
|
||||
```rust
|
||||
use rhai::{Engine, Dynamic};
|
||||
|
||||
// Different versions of the same function 'foo'
|
||||
// will be matched in the following order.
|
||||
|
||||
fn foo1(x: i64, y: &str, z: bool) { }
|
||||
|
||||
fn foo2(x: i64, y: &str, z: Dynamic) { }
|
||||
|
||||
fn foo3(x: i64, y: Dynamic, z: bool) { }
|
||||
|
||||
fn foo4(x: i64, y: Dynamic, z: Dynamic) { }
|
||||
|
||||
fn foo5(x: Dynamic, y: &str, z: bool) { }
|
||||
|
||||
fn foo6(x: Dynamic, y: &str, z: Dynamic) { }
|
||||
|
||||
fn foo7(x: Dynamic, y: Dynamic, z: bool) { }
|
||||
|
||||
fn foo8(x: Dynamic, y: Dynamic, z: Dynamic) { }
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register all functions under the same name (order does not matter)
|
||||
|
||||
engine.register_fn("foo", foo5)
|
||||
.register_fn("foo", foo7)
|
||||
.register_fn("foo", foo2)
|
||||
.register_fn("foo", foo8)
|
||||
.register_fn("foo", foo1)
|
||||
.register_fn("foo", foo3)
|
||||
.register_fn("foo", foo6)
|
||||
.register_fn("foo", foo4);
|
||||
```
|
||||
|
||||
|
||||
~~~admonish warning "Only the right-most 16 parameters can be `Dynamic`"
|
||||
|
||||
The number of parameter permutations goes up exponentially, and therefore there is a realistic limit
|
||||
of 16 parameters allowed to be [`Dynamic`], counting from the _right-most side_.
|
||||
|
||||
For example, Rhai will not find the following function – Oh! and those 16 parameters to the right
|
||||
certainly have nothing to do with it!
|
||||
|
||||
```rust
|
||||
// The 'd' parameter counts 17th from the right!
|
||||
fn weird(a: i64, d: Dynamic, x1: i64, x2: i64, x3: i64, x4: i64,
|
||||
x5: i64, x6: i64, x7: i64, x8: i64,
|
||||
x9: i64, x10: i64, x11: i64, x12: i64,
|
||||
x13: i64, x14: i64, x15: i64, x16: i64) {
|
||||
|
||||
// ... do something unspeakably evil with all those parameters ...
|
||||
}
|
||||
```
|
||||
~~~
|
||||
|
||||
|
||||
TL;DR
|
||||
-----
|
||||
|
||||
```admonish question "How is this implemented?"
|
||||
|
||||
#### Hash lookup
|
||||
|
||||
Since functions in Rhai can be [overloaded][function overloading], Rhai uses a single _hash_ number
|
||||
to quickly lookup the actual function, based on argument types.
|
||||
|
||||
For each function call, a hash is calculated from:
|
||||
|
||||
1. the function's [namespace][function namespace], if any,
|
||||
2. the function's name,
|
||||
3. number of arguments (its _arity_),
|
||||
4. unique ID of the type of each argument, if any.
|
||||
|
||||
The correct function is then obtained via a simple hash lookup.
|
||||
|
||||
#### Limitations
|
||||
|
||||
This method is _fast_, but at the expense of flexibility (such as multiple argument types that must
|
||||
map to a single version). That is because each type has a different ID, and thus they calculate to
|
||||
different hash numbers.
|
||||
|
||||
This is the reason why [generic functions](generic.md) must be expanded into concrete types.
|
||||
|
||||
The type ID of [`Dynamic`] is different from any other type, but it must match all types seamlessly.
|
||||
Needless to say, this creates a slight problem.
|
||||
|
||||
#### Trying combinations
|
||||
|
||||
If the combined hash calculated from the actual argument type ID's is not found, then the [`Engine`]
|
||||
calculates hashes for different _combinations_ of argument types and [`Dynamic`], systematically
|
||||
replacing different arguments with [`Dynamic`] _starting from the right-most parameter_.
|
||||
|
||||
Thus, assuming a three-argument function call:
|
||||
|
||||
~~~rust
|
||||
foo(42, "hello", true);
|
||||
~~~
|
||||
|
||||
The following hashes will be calculated, in order.
|
||||
They will be _all different_.
|
||||
|
||||
| Order | Hash calculation method |
|
||||
| :---: | --------------------------------------------------- |
|
||||
| 1 | `foo` + 3 + `i64` + `string` + `bool` |
|
||||
| 2 | `foo` + 3 + `i64` + `string` + [`Dynamic`] |
|
||||
| 3 | `foo` + 3 + `i64` + [`Dynamic`] + `bool` |
|
||||
| 4 | `foo` + 3 + `i64` + [`Dynamic`] + [`Dynamic`] |
|
||||
| 5 | `foo` + 3 + [`Dynamic`] + `string` + `bool` |
|
||||
| 6 | `foo` + 3 + [`Dynamic`] + `string` + [`Dynamic`] |
|
||||
| 7 | `foo` + 3 + [`Dynamic`] + [`Dynamic`] + `bool` |
|
||||
| 8 | `foo` + 3 + [`Dynamic`] + [`Dynamic`] + [`Dynamic`] |
|
||||
|
||||
Therefore, the version with all the correct parameter types will always be found first if it exists.
|
||||
|
||||
At soon as a hash is found, the process stops.
|
||||
|
||||
Otherwise, it goes on for up to 16 arguments, or at most 65,536 tries.
|
||||
That's where the 16 parameters limit comes from.
|
||||
```
|
||||
|
||||
```admonish question "What?! It calculates 65,536 hashes for each function call???!!!"
|
||||
|
||||
Of course not. Don't be silly.
|
||||
|
||||
#### Not every function has 16 parameters
|
||||
|
||||
Studies have repeatedly shown that most functions accept few parameters, with the mean between
|
||||
2-3 parameters per function. Functions with more than 5 parameters are rare in normal code bases.
|
||||
If at all, they are usually [closures] that _capture_ lots of external variables, bumping up the
|
||||
parameter count; but [closures] are always script-defined and thus all parameters are already
|
||||
[`Dynamic`].
|
||||
|
||||
In fact, you have a bigger problem if you write such a function that you need to call regularly.
|
||||
It would be far more efficient to group those parameters into [object maps].
|
||||
|
||||
#### Caching to the rescue
|
||||
|
||||
Function hashes are _cached_, so this process only happens _once_, and only up to the number of
|
||||
rounds for the correct function to be found.
|
||||
|
||||
If not, then yes, it will calculate up to 2<sup>_n_</sup> hashes where _n_ is the number of
|
||||
arguments (up to 16). But again, this will only be done _once_ for that particular
|
||||
combination of argument types.
|
||||
```
|
||||
|
||||
```admonish danger "But then... beware module functions"
|
||||
|
||||
The functions resolution _cache_ resides only in the [global namespace][function namespace].
|
||||
This is a limitation.
|
||||
|
||||
Therefore, calls to functions in an [`import`]ed [module] (i.e. _qualified_ with
|
||||
a [namespace][function namespace] path) do not have the benefit of a cache.
|
||||
|
||||
Thus, up to 2<sup>_n_</sup> hashes are calculated during _every_ function call.
|
||||
This is unlikely to cause a performance issue since most functions accept only a few parameters.
|
||||
```
|
Reference in New Issue
Block a user