Meta-comment:
I think part of what is confusing is that there's nothing intrinsically slice-specific in your trait examples themselves, only in how you use them. Namely, IIUC, you want the logical expectation of Length + IndexXxxValue<usize>
to be that the valid indices are exactly everything in 0..thing.len()
. That expectation would be in the documentation presumably. But in the absence of that expectation, the Length
methods make sense for any collection, independently of what types the indices can be or which values will succeed.
You aren't talking about actual slices, but about things that index by usize
like slices do.
Also your examples don't demonstrate a use case where you can't return by reference. Your IndexGetValue
example looks like something you would blanket-implement for any C: Index<Idx, Output: Clone>
, for instance. But the use cases you actually care about are those where Index
is not an option.
Substance-comment:
I think you need to decide how general you want to be. The topic so far reads to me like you want to be as general as possible and do something Index
-esque, but also incidentally support your use case. At that level of generality, the assumptions about how indexing by usize
work (0..len()
are the current indices) won't always be valid, which you'll have to handle some way; i.e. they're a somewhat orthogonal concern to the reference constraints of Index
.
Without language support so that one can use actual indexing, the "work around Index
's reference constraint" portion of this feels like trying to get the ecosystem to agree on some standard getter-setter traits, and I'm not too surprised that hasn't happened.
Thinking about the general solution anyway... sometimes people run into the reference constraint because they want to return owned values, but another possibility is that they wish they could return other borrow-capturing data structures instead of references, like a MutexGuard<'_, T>
. (Though this comes up more with traits like Borrow
I think.)
A more general approach for that issue would be something like
trait GetByKey<Idx> {
type Output<'this> where Self: 'this;
fn contains_key(&self, idx: Idx) -> bool;
fn get_value(&self, idx: Idx) -> Self::Output<'_>;
}
(It would have the usual GAT rough edges if GATs were actually used, but this form is more digestible.)
The setting case doesn't need the lending ability, if it only supports setting (versus handing back something like a lock that can be passed around for the sake of mutation elsewhere).
pub trait SetByKey<Idx> {
type Value;
fn is_settable(&self, index: Idx) -> bool;
fn set_value(&mut self, index: Idx, value: Self::Value);
}
(If you wanted to support things like += ...
, you would need the lending ability. That doesn't seem like your actual use case so I'll ignore it.)
If you then try to generalize the "what are the current indices" portion of your actual use case, you could end up with something like:
trait KeyIterable {
type KeyIter<'this> where Self: 'this;
fn current_keys(&'this self) -> Self::KeyIter<'_>;
}
And your by-value slice-like case could be...
trait OwnedSliceLike
where
Self: SetByKey<usize>
+ for<'a> GetByKey<usize, Output<'a> = Self::Value>
+ for<'a> KeyIterable<KeyIter<'a> = Range<usize>>,
{}
The set of valid keys being a Range<usize>
encodes your "slice-like usize
indexing" condition.
Here's what it all may look like together (in alternative-to-GAT form). It does have a very "Collection
trait" feel.
Alternatively, you could take a more targeted approach to what seems to be your actual use-case: slice-like usize
based indexing. In which case, you can throw out the generic Idx
on everything, and document or encode the valid key assumptions into the traits.
pub trait GetSliceLike<'this, Guard = &'this Self>: SliceLike {
type Output;
fn get_value(&'this self, index: usize) -> Self::Output;
}
pub trait SetSliceLike: SliceLike {
type Value;
fn is_settable(&self, index: usize) -> bool;
fn set_value(&mut self, index: usize, value: Self::Value);
}
pub trait SliceLike {
fn len(&self) -> usize {
let range = self.current_range();
range.end.saturating_sub(range.start)
}
fn is_empty(&self) -> bool {
self.current_range().is_empty()
}
fn contains_index(&self, index: usize) -> bool {
self.current_range().contains(&index)
}
fn current_range(&self) -> Range<usize>;
}
And if you don't need to support borrowing gets, throw that out too.
// No lifetime and uses `Value` now, could probably merge into `SliceLike`
pub trait GetSliceLike: SliceLike {
fn get_value(&self, index: usize) -> Self::Value;
}
// `Value` moved to `SliceLike`
pub trait SetSliceLike: SliceLike {
fn is_settable(&self, index: usize) -> bool;
fn set_value(&mut self, index: usize, value: Self::Value);
}
pub trait SliceLike {
type Value;
// ...same methods as before...
}