I'm looking at creating a custom type for storing index into an array (Vec) which always holds valid values for a specific instance of the array (so this is something that needs runtime validity, not compile-time).
The index will be stored in another struct so I want to put guarantees around its validity so that invalid values cannot be set and the reader does not need to do validity checks each time before using the value to index the array.
let myvec = vec![1,2,3];
let mut safe_index = SafeIndex::new(<initial value>, myvec.len()); // fails to construct if "initial value" is out of bounds
safe_index += 1; // ok
safe_index += 10; // should fail
safe_index -= 10;
// copy should create another instance with current value and same range bounds
let safe_index_2 = safe_index; // same bounds as safe_index
I'm not 100% sure how should it "fail" (panic? Option? Result?). From an ergonomics perspective, I'd rather have it not return a Result.
I'm new to Rust so I might've missed something obvious. Feel free to point me to existing docs which solve this problem.
I'm always confused when people talk about how Result has "bad ergonomics". Result is the primary and intended way of handling errors in this language.
Panicking is not really useful in this case: if the intended use of the type is to be able to index into an array/slice/vector without it panicking, then generating a panic in the index manipulation itself didn't solve anything.
Option is probably not a good alternative, either. It is meant to denote the absence of a value, not an error. It can also be ignored, unlike Result, which is #[must_use].
It looks like the cleanest option would be to return a Result<(), OutOfBoundsError> (where you define your own error type, presumably).
This implies that the index manipulation API should mostly consist of functions rather than overloaded operators, since the signatures of the latter are pretty constrained. Eg., you can't return a Result from AddAssign::add_assign().
It looks to me like there's nothing preventing the programmer from using a SafeIndex where the length doesn't actually match the Vec's length, at which point the indexing is going to have to panic anyway?
In some cases where an implementation should not have bad side effects, I think a panic is more "succinct" (although for every other case Result is really useful and the thing to do). TBH I'm not sure if the case I described qualifies for such an assertion, i.e index access should not have bad side effects.
Return a Result from where? A custom function (trait?) on the vector?
Maybe I can explain my usecase a bit better:
There's a fxn called with a list = eg A(mylist: Vec<myitems>)
In A() I initialize an "index" (mylist_index: usize) which should be constrained to the set of values defined by length of mylist.
A() calls another function passing it mylist & the index B(mylist, mylist_index).
B() can modify the index and access items in mylist. I was thinking if there's a way to avoid splattering index validity checks whenever manipulating the index value or using it to access mylist.
It'll be used to access elements from a list (see a bit more detailed usecase I described here.
You're right. I think for it to be truly safe it somehow has to be tied to the particular instance of the vector itself. I think in my effort to make a simple example I completely forgot about this detail.