`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_n_ 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_n_ hashes are calculated during _every_ function call. This is unlikely to cause a performance issue since most functions accept only a few parameters. ```