How idiomatic are `Index` and `IndexMut`?

I just discovered the Index and IndexMut traits from std::ops and I realized I've never seen an API use these. I was wondering if there are any good, idiomatic uses for them? It seems to me that .get() and .get_mut() are preferred for APIs since they give the user more control.

1 Like

They implement the language's indexing operator, like my_vec[1] or my_map[key]. So while you probably wouldn't use the traits directly, you are probably using them very often.

8 Likes

Actually I would say these are not really idiomatic as is. Since those are stabilized before GAT (obviously), it's usage is very limited and mostly used for slice indexing only.

Not sure if we could find a way to modify Index[Mut] after GAT stabilization.

Using [] is pretty idiomatic I'd say. GAT is unstable and not idiomatic today. Maybe I don't understand your argument.

Index expressions are place expressions; that is, foo[x] is roughly *foo.index.(x), and Index::index already returns a lifetime-bound result -- namely, &Self::Output (lifetime tied to &self). Due to the place-expression dereference, there's no need for GAT on the associated type Output.

The closest thing I could think of where GAT might enter into it is something like (assuming some GAT-defaults feature):

pub trait Index<Idx> {
    type Output: ?Sized;
    type Bikeshed<'a>: Deref<Self::Output> = &'a Self::Output where Self: 'a;
    fn index(&self, index: Idx) -> Self::Bikeshed<'_>;
}

To allow the return of non-references.

Or did you have something else in mind?

5 Likes

Ok I will try to explain myself more.

I'm not against using []. I'm suggesting it's actually hard to implement Index[Mut] since it must return a reference, which is why OP didn't see any API use it.

Maybe it's just OP does not know [] desugars to Index[Mut] and I'm overcomplicating the problem. :sweat:

1 Like

AFAIK that doesn't work because &foo[bar] desugars to &*foo.index(bar), so if foo.index(bar) returned an owned value &foo[bar] would cause it to be dropped. Similarly foo[bar] would try to move out of the value returned by foo.index(bar).

Moreover IndexMut requires a different associated type and that complicates method/trait resolution a lot.

5 Likes

I don't follow. Returning a reference is not any harder than returning any other type. Moreover, several types in std implement IndexMut. Namely, basically all the contiguous-storage types (slices, vectors, and types backed by them such as strings and VecDeque) do.

It's much harder to return a reference I would say. Returning a reference requires the container actually contains the object, this forbids returning a Cow or any kind of guard object. All types in you link to doc falls into the "slice indexing" category, or "contiguous-storage types" if you like this term better. These types are nearly the only ones that is easy to implement Index[Mut] (, beside Index for HashMap-like.

If you search std::ops::Index in the user forum, you can easily see many people having problem with implementing it.

There's even an RFC for improving it.

2 Likes

Yeah. Not like this is the fault of Index? This is… physics, I guess.

You might say it's much harder to cut a tree with a kitchen knife than it would be with a chainsaw. Does this mean that you shouldn't ever use a kitchen knife? I'd be wary of anyone using a chainsaw for cutting bread.

You are asking Index to be used for something it is not suited for. No wonder it's "hard". But don't blame it on the language. Adjust your own expectations instead – that's the only reasonable thing to do.

Yes, all your points are fair. I will retract the "unidomatic" claim, and change it to the following statement then:

Yes, Index is idomatic, as long as there's a easy way to implement it. But if you are looking for generic indexing like C++ or Python, then do not use it.

Just to be clear here: "idiom" describes something that is, not what something ought to be. Many folks have opinions like "folks ought to never use slice indexing notation under any circumstances." But that sort of opinion does not reflect what is idiomatic in Rust.

Using index notation, e.g., slice[i], is absolutely idiomatic.

Generally speaking, when you index something with an invalid index, the result of that operation will panic. It is also idiomatic to treat that as a bug. Therefore, you either need to make sure your index is correct, or if it's derived directly from end user input, somewhere you'll need to return an error. But in most cases, indices are an artifact of some internal book-keeping mechanism, and are either guaranteed to be correct by some internal invariant or precondition. In which case, panicking is not only okay but wonderful, because that panic will point you directly to a bug that ought to be fixed.

To a first approximation: "correct Rust programs don't panic."

10 Likes

I believe the confusion here was that "it's harder to return a reference" can mean either "a reference is a type that is more difficult to return" or "being required to return a reference makes the rest of the code harder to write" (and probably plenty more) - so basically you're both right.

I will say, though, that statements like:

are fairly unhelpful? There's plenty of things that Rust does that are not ideal and the team knows about it and wants to fix it, but can't yet. This is one of those.

Having an expectation that the language could be better is the same thing as not thinking that you're wrong to want this, and that's perfectly reasonable here.

4 Likes

Indexed place expressions needing to desugar to dereferenced references is not a design error. References having to point to existing owned data isn't one, either. Rather, these are technical necessities. By adjusting one's expectations, I meant that one should understand why these technical necessities arise in Rust. Expecting that indexing should work exactly as in other languages is not going to be productive.

I know Rust made a particular choice here, but I'm not sure if it was the best one.

To me indexing is not the first-class solution to anything. It's not optimal for loops. But it's also risking panics when indexing with arbitrary keys.

The limitation of using references makes it unsuitable for more "clever" uses (converting, inserting) that could give it usefulness beyond what it currently is.

3 Likes

Right, I agree with this. To me, indexing is not more than syntactic sugar for array access, which can be useful/convenient to someone coming from other languages. It's also genuinely useful per se in situations where direct iteration doesn't work for some reason (e.g. for working around mutable aliasing issues, or for implementing algorithms that are formulated in terms of random access).

I view indexing as a quick-and-dirty solution, and I usually don't use it for anything more serious/clever if I can avoid it. I don't mind the lack of syntactic sugar for such more "clever" things, either; it's probably better that these more complex operations (like insert-and-update) have their own, descriptive method name.

I think it's obvious that it wasn't the best one if we would consider Rust in isolation, but it's, basically, repeating mistake that C++ did thus, maybe, it was the best decision, all things considering.

It's better to have the devil you know than something which is better but which may confuse potential language users.

It can be changed, probably, but I'm not even sure if that would be actually an improvement: this would mean there are two ways of doing things and they would interfere and conflict.

Perhaps someone who feels really strongly about it can try to make it a nightly-only feature to see if it's actually usable or not.

There's a long standing RFC above, and yes, the interaction of the various types of indexing and yes, the behavior when some indexing traits are implemented and not others is indeed if the bigger issues.

I'm not sure what you mean by this? C++ operator[] can return by value, and even uses that in the standard library to implement a entry proxy for std::bitset to allow mutation on single bits.

My understanding is Rust couldn't allow Index:: Item to be returned directly, which would be a lot closer to C++, because then there would not be any way to implement the normal case of returning an interior reference with the same lifetime as self, because you wouldn't have the lifetime when declaring Item. GATs would have allowed it, but it's a bit late for Index.

6 Likes

Temporary lifetime extension covers at least some cases (where ordinary temporary lifetimes aren't enough).

That's already how indexing works (only you'd be trying to move out of a dereference instead of out from behind a reference).

I think it could still work, albeit with all the other inference and bounds pitfalls of GATs today.


All that being said, it was more a thought experiment than a serious proposal. [1] It's backwards incompatible anyway as Index and IndexMut are currently dyn-safe. [2]


  1. Well, this post was; originally I was just trying to suss out what they meant. ↩︎

  2. At least until we get GAT defaults, dyn-safe GATs, and default dyn GAT parameters :wink:. ↩︎

Yes, pile of kludges allows C++ to mitigate mistake to some degree. But C++ still suffers from the initial mistake (similar to the mistake they did with ++ and -- operators).

C doesn't have indexing operator at all. a[b] is, by definition, *(a+b). C++ haven't implemented indexing operator precisely like that, but it still does it in a similar fashion: indexing operator either provides reference or proxy object which then uses kludges to keep an illusion that you deal with real object.

That's wrong level of abstraction and, ultimately, wrong design. What you need to do things like RPC-proxy or SQL-proxy or things like that is what Python does: getitem and setitem (plus also maybe delitem).

And, arguably, if Rust could have allowed to break backward compatibility then such redesign would have made sense.

Because, fundamentally, reading and writing are two different operations which are associated with array elements since early days of FORTRAN.

But I'm not sure such change now would make things better and not worse: lots of people don't even understand what's wrong with current design and if there would two different ways of implementing element access it would have created significant confusion.

The big problem is not with Index trait but with IndexMut trait. It's supposed to return reference to element, but said element may exist or not exist if what we have is not a simple array but something more complex!

Instead it should have received element which is supposed to be assigned to the element (which may not even exist at this point).

But of course this would mean that for something like a[b].c = d you would need yet another trait!

It may even be possible to fix the current design by adding index_assign function to IndexMut trait (used when it's present, otherwise default implementation based on IndexMut is used), but Rust tend not to do such backward-incompatible changes.

Although there are precedent with arrays and IntoIterator thus it's not entirely implausible, but… I'm not sure issue is acute enough to warrant such a complication.

2 Likes

Seems like you should read the RFC above. There's some confusion and digression, but it basically is adding get and set.

I would say though, that returning a reference rather than using get and set is not a hack or problem. Consider a hash table: having get and set would require computing the hash both times when doing something like counts[word] += 1;. Extending the reference concept to allowing a proxy/entry type just allows more complex cases like concurrent access (locking at the entry level), insertions, and derived values like in bitsets.