TL;DR: not allowing custom reference types is due to the lack of LGATs to tie the returned proxy lifetime to the indexing.
Interestingly enough, this shares a lot with "placement". In previous versions of the compiler, there was a placement type that allowed you to write e.g. vec.place_back() <- value;
I don't know if it was ever actually supported, but this extends simply to allowing map[key] <- value
.
Despite the syntactical simplicity of and theoretical purity of allowing $receiver[$index] = $expr
as sugar for IndexSet::index_set(k#autoref $receiver, $index, $expr)
, it's more complicated than that in practice.
container[index]
evaluates to a place. This place is then used as any other place is, and is subject to the same autoref and usage rules as any other place. The compiler then (after type inference!) substitutes container[index]
with roughly *Index::index(&container, index)
or *IndexMut::index_mut(&mut container, index)
depending on if the place was used as a shared place or as a mutable place.
There isn't even a concept in the language for a place which is written to but not read from. Without such a concept, the compiler cannot tell the difference between use as a mutable place (thus indexing translating to IndexMut
) or as a write target place (thus indexing translating to IndexSet
). Perhaps just specifically the index expression could be special cased, but this doesn't compose at all. Most of the people working on the language are trying to remove special cases, not add new ones. Usually around once a year someone brings up the concept of some "&lateinit mut T
" type which would add the necessary semantics into the language for separating mutable places from write target places. However, the utility of such a type is somewhat limited (and relies on typestate, a huge feature on its own right, for much of it), so what gets significantly more attention is "&move T
"/IndexMove
, because Box
already can do that, and extending this ability to user types (and thus making Box
incrementally less special) has a much higher impact.
&lateinit mut T
also runs into significant issues around necessarily being a linear type as well. Plus, there's evaluation order issues as well: container[ix] = expr
is IIUC ordered as
- evaluate
container
- evaluate
ix
- evaluate
container[ix]
- evaluate
expr
- drop_in_place
container[ix]
- move
expr
into container[ix]
This evaluation order is different from a setter function. If you write container.index_set(ix, expr)
, this is ordered as
- evaluate
container
- evaluate
ix
- evaluate
expr
(swapped!)
- call
container.index_set
with ix
, expr
- evaluate the place (swapped!)
- drop_in_place the place
- move
expr
into the place
For a language that manages memory for you and doesn't try to expose exact control over the execution model, it's relatively simple to have indexing assignment as a special case of syntax sugar. For a language that is going for exposing such control, though, there are a lot of devils in the details of how exactly this would work.
Without an argument supported by some measurement, this is just an assertion without basis.
And one I disagree with. An API which offers just get
and get_mut
but not set
can absolutely be good and useful.
It's worth noting that C++ is really cheating here. Just map[key]
for most containers inserts a default constructed value at key
and returns a normal C++ reference. There's actually nothing preventing you from providing the same semantics in Rust, at least for IndexMut
inserting a mut
reference to a newly inserted Default
constructed value.
You shouldn't, but you can implement the C++ semantics here.