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/rust/indexers.md
2025-04-03 09:18:05 +02:00

198 lines
7.7 KiB
Markdown

Custom Type Indexers
====================
{{#include ../links.md}}
A [custom type] can also expose an _indexer_ by registering an indexer function.
A [custom type] with an indexer function defined can use the bracket notation to get/set a property
value at a particular index:
> _object_ `[` _index_ `]`
>
> _object_ `[` _index_ `]` `=` _value_ `;`
The [_Elvis notation_][elvis] is similar except that it returns [`()`] if the object itself is [`()`].
> `// returns () if object is ()`
> _object_ `?[` _index_ `]`
>
> `// no action if object is ()`
> _object_ `?[` _index_ `]` `=` _value_ `;`
Like property [getters/setters], indexers take a `&mut` reference to the first parameter.
They also take an additional parameter of any type that serves as the _index_ within brackets.
Indexers are disabled when the [`no_index`] and [`no_object`] features are used together.
| `Engine` API | Function signature(s)<br/>(`T: Clone` = custom type,<br/>`X: Clone` = index type,<br/>`V: Clone` = data type) | Can mutate `T`? |
| -------------------------- | ------------------------------------------------------------------------------------------------------------- | :----------------------------: |
| `register_indexer_get` | `Fn(&mut T, X) -> V` | yes, but not advised |
| `register_indexer_set` | `Fn(&mut T, X, V)` | yes |
| `register_indexer_get_set` | getter: `Fn(&mut T, X) -> V`<br/>setter: `Fn(&mut T, X, V)` | yes, but not advised in getter |
```admonish danger.small "No support for references"
Rhai does NOT support normal references (i.e. `&T`) as parameters.
All references must be mutable (i.e. `&mut T`).
```
```admonish warning.small "Getters must be pure"
By convention, index getters are not supposed to mutate the [custom type],
although there is nothing that prevents this mutation.
```
~~~admonish tip.small "Tip: `EvalAltResult::ErrorIndexNotFound`"
For [fallible][fallible function] indexers, it is customary to return
`EvalAltResult::ErrorIndexNotFound` when called with an invalid index value.
~~~
Cannot Override Arrays, BLOB's, Object Maps, Strings and Integers
-----------------------------------------------------------------
```admonish failure.side "Plugins"
They can be defined in a [plugin module], but will be ignored.
```
For efficiency reasons, indexers **cannot** be used to overload (i.e. override)
built-in indexing operations for [arrays], [object maps], [strings] and integers
(acting as [bit-field] operation).
The following types have built-in indexer implementations that are fast and efficient.
<section></section>
| Type | Index type | Return type | Description |
| ----------------------------------------- | :---------------------------------------: | :---------: | ---------------------------------------------------------------------------- |
| [`Array`] | `INT` | [`Dynamic`] | access a particular element inside the [array] |
| [`Blob`] | `INT` | `INT` | access a particular byte value inside the [BLOB] |
| [`Map`] | [`ImmutableString`],<br/>`String`, `&str` | [`Dynamic`] | access a particular property inside the [object map] |
| [`ImmutableString`],<br/>`String`, `&str` | `INT` | [character] | access a particular [character] inside the [string] |
| `INT` | `INT` | boolean | access a particular bit inside the integer number as a [bit-field] |
| `INT` | [range] | `INT` | access a particular range of bits inside the integer number as a [bit-field] |
```admonish warning.small "Do not overload indexers for built-in standard types"
In general, it is a bad idea to overload indexers for any of the [standard types] supported
internally by Rhai, since built-in indexers may be added in future versions.
```
Examples
--------
```rust
#[derive(Debug, Clone)]
struct TestStruct {
fields: Vec<i64>
}
impl TestStruct {
// Remember &mut must be used even for getters
fn get_field(&mut self, index: String) -> i64 {
self.fields[index.len()]
}
fn set_field(&mut self, index: String, value: i64) {
self.fields[index.len()] = value
}
fn new() -> Self {
Self { fields: vec![1, 2, 3, 4, 5] }
}
}
let mut engine = Engine::new();
engine.register_type::<TestStruct>()
.register_fn("new_ts", TestStruct::new)
// Short-hand: .register_indexer_get_set(TestStruct::get_field, TestStruct::set_field);
.register_indexer_get(TestStruct::get_field)
.register_indexer_set(TestStruct::set_field);
let result = engine.eval::<i64>(
r#"
let a = new_ts();
a["xyz"] = 42; // these indexers use strings
a["xyz"] // as the index type
"#)?;
println!("Answer: {result}"); // prints 42
```
Convention for Negative Index
-----------------------------
If the indexer takes a signed integer as an index (e.g. the standard `INT` type), care should be
taken to handle _negative_ values passed as the index.
It is a standard API _convention_ for Rhai to assume that an index position counts _backwards_ from
the _end_ if it is negative.
`-1` as an index usually refers to the _last_ item, `-2` the second to last item, and so on.
Therefore, negative index values go from `-1` (last item) to `-length` (first item).
A typical implementation for negative index values is:
```rust
// The following assumes:
// 'index' is 'INT', 'items: usize' is the number of elements
let actual_index = if index < 0 {
index.checked_abs().map_or(0, |n| items - (n as usize).min(items))
} else {
index as usize
};
```
The _end_ of a data type can be interpreted creatively. For example, in an integer used as a
[bit-field], the _start_ is the _least-significant-bit_ (LSB) while the `end` is the
_most-significant-bit_ (MSB).
Convention for Range Index
--------------------------
```admonish tip.side.wide "Tip: Negative values"
By convention, negative values are _not_ interpreted specially in indexers for [ranges].
```
It is very common for [ranges] to be used as indexer parameters via the types
`std::ops::Range<INT>` (exclusive) and `std::ops::RangeInclusive<INT>` (inclusive).
One complication is that two versions of the same indexer must be defined to support _exclusive_
and _inclusive_ [ranges] respectively.
```rust
use std::ops::{Range, RangeInclusive};
let mut engine = Engine::new();
engine
/// Version of indexer that accepts an exclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: Range<i64>| -> bool { ... },
|obj: &mut TestStruct, range: Range<i64>, value: bool| { ... },
)
/// Version of indexer that accepts an inclusive range
.register_indexer_get_set(
|obj: &mut TestStruct, range: RangeInclusive<i64>| -> bool { ... },
|obj: &mut TestStruct, range: RangeInclusive<i64>, value: bool| { ... },
);
engine.run(
"
let obj = new_ts();
let x = obj[0..12]; // use exclusive range
obj[0..=11] = !x; // use inclusive range
")?;
```