andex (Array iNDEX) is a zero-dependency rust crate that helps us create strongly-typed, zero-cost, numerically bound array index and the corresponding array type with the provided size. The index is safe in the sense that an out-of-bounds value can't be created, and the array type can't be indexed by any other types.
This is useful in scenarios where we have different arrays inside a
struct
and we want reference members without holding proper
references that could "lock" the whole struct
. It may also be useful
when programming an
Entity Component System.
And it's all done without requiring the use of any macros.
[Andex
] is the index type and [AndexableArray
] is the type of
the array wrapper.
The recommended approach to use andex is as follows:
- Create a unique empty type
rust
enum MyIdxMarker {}
- Create a type alias for the [Andex
] type that's parameterized
with that type:
rust
type MyIdx = Andex<MyIdxMarker, 12>;
- Create a type alias for the [AndexableArray
] type that's
indexed by the [Andex
] alias created above:
rust
type MyU32 = AndexableArray<MyIdx, u32, { MyIdx::SIZE }>;
// There is also a helper macro for this one:
type MyOtherU32 = andex::array!(MyIdx, u32);
When an andex is created, it knows at compile time the size of the array it indexes, and all instances are assumed to be within bounds.
For this reason, it's useful to limit the way Andex
's are
created. The ways we can get an instance is:
Via new
, passing the value as a generic const argument:
rust
const first : MyIdx = MyIdx::new::<0>();
This checks that the value is valid at compile time, as long as you
use it to create const
variables.
Via try_from
, which returns Result<Andex, Error>
that has to be
checked or explicitly ignored:
rust
if let Ok(first) = MyIdx::try_from(0) {
// ...
}
Via FIRST
and LAST
:
rust
const first : MyIdx = MyIdx::FIRST;
let last = MyIdx::LAST;
By iterating:
rust
for idx in MyIdx::iter() {
// ...
}
The assumption that the instances can only hold valid values allows us
to use get_unsafe
and get_unsafe_mut
in the indexer
implementation, which provides a bit of optimization by preventing the
bound check when indexing.
[ let myu32 = MyU32::default(); // We also have a helper macro that avoids repeating the size:
type MyOtherU32 = andex::array!(MyIdx, u32);
Besides indexing them with a coupled ```rust
use std::convert::TryFrom;
use std::error::Error;
use andex::*; // Create the andex type alias:
// First, we need an empty type that we use as a marker:
enum MyIdxMarker {}
// The andex type takes the marker (for uniqueness)
// and the size of the array as parameters:
type MyIdx = Andex // Create the array wrapper:
type MyU32 = AndexableArray // We can create other arrays indexable by the same Andex:
type MyF64 = AndexableArray fn main() -> Result<(), Box }
``` This is the reason to use Andex instead of a plain array in the
first play, right? Below is a list of some of the compile-time
restrictions that we get. We can't index [ The following code doesn't compile: ```rust
use andex::*;
enum MyIdxMarker {}
type MyIdx = Andex fn main() {
let myu32 = MyU32::default(); }
``` We can't create a const [ The following code doesn't compile: ```rust
use andex::*;
enum MyIdxMarker {}
type MyIdx = Andex fn main() {
// Error: can't create out-of-bounds const:
const myidx : MyIdx = MyIdx::new::<13>();
}
``` We can't index [ The following code doesn't compile: ```rust
use andex::*; enum MyIdxMarker {}
type MyIdx = Andex enum TheirIdxMarker {}
type TheirIdx = Andex fn main() {
let myu32 = MyU32::default();
let theirIdx = TheirIdx::FIRST; }
``` These alternatives may fit better cases where we need unbound indexes
(maybe for vector):AndexableArray
] instances are less restrictive. They can be created
in several more ways:
- Using Default
if the underlying type supports it:
```rust
type MyU32 = AndexableArray
- Using `From` with an appropriate array:
rust
let myu32 = MyU32::from([8; MyIdx::SIZE]);
- Collecting an iterator with the proper elements and size:
rust
let myu32 = (0..12).collect::
Note:
collect` panics if the iterator returns a different
number of elements.Using andexable arrays
Andex
instance, we can
also access the inner array by using as_ref
, iterate it in a
for
loop (using one of the IntoIterator
implementations) or
even get the inner array by consuming the AndexableArray
.Full example
// We can now only index MyU32 using MyIdx
const first : MyIdx = MyIdx::new::<0>();
println!("{:?}", myu32[first]);
// Trying to create a MyIdx with an out-of-bounds value
// doesn't work, this won't compile:
// const _overflow : MyIdx = MyIdx::new::<30>();
// Trying to index myu32 with a "naked" number
// doesn't work, this won't compile:
// println!("{}", myu32[0]);
// We can create indexes via try_from with a valid value:
let second = MyIdx::try_from(2);
// ^ Returns a Result, which Ok(MyIdx) if the value provided is
// valid, or an error if it's not.
// We can also create indexes at compile-time:
const third : MyIdx = MyIdx::new::<1>();
// The index type has an `iter()` method that produces
// all possible values in order:
for i in MyIdx::iter() {
println!("{:?}", i);
}
Ok(())
Compile-time guarantees
AndexableArray
] with a usize
.// Error: can't index myu32 with a usize
println!("{}", myu32[0]);
Andex
] with an out-of-bounds value.
AndexableArray
] with a different Andex, even when
it has the same size. This is what using different markers gets
us.// Error: can't index a MyU32 array with TheirIdx
println!("{}", myu32[theirIdx]);
Alternatives