Is there any technical reason Rust is designed to use dot notation for tuples instead of using index notation (t[2]
)?
let t = (20u32, true, \'b\
I had no part in the design decisions, but here's my perspective:
Tuples contain mixed types. That is, the property type_of(t[i]) == type_of(t[j])
cannot be guaranteed.
However, conventional indexing works on the premise that the i
in t[i]
need not be a compile-time constant, which in turn means that the type of t[i]
needs to be uniform for all possible i
. This is true in all other rust collections that implement indexing. Specifically, rust types are made indexable through implementing the Index trait, defined as below:
pub trait Index<Idx> where Idx: ?Sized {
type Output: ?Sized;
fn index(&'a self, index: Idx) -> &'a Self::Output;
}
So if you wanted a tuple to implement indexing, what type should Self::Output
be? The only way to pull this off would be to make Self::Output
an enum, which means that element accesses would have to be wrapped around a useless match t[i]
clause (or something similar) on the programmer's side, and you'll be catching type errors at runtime instead of compile-time.
Furthermore, you now have to implement bounds-checking, which is again a runtime error, unless you're clever in your tuple implementation.
You could bypass these issues by requiring that the index by a compile-time constant, but at that point tuple item accesses are pretending to behave like a normal index operation while actually behaving inconsistently with respect to all other rust containers, and there's nothing good about that.
I want to provide an answer from my experience using a functional language (Ocaml) for the while since I've posted this question.
Apart from @rom1v reference, indexing syntax like a[0]
everywhere else also used in some kind of sequence structure, of which tuples aren't. In Ocaml, for instance, a tuple (1, "one")
is said to have type int * string
, which conforms to the Cartesian product in mathematics (i.e., the plane is R^2 = R * R). Plus, accessing a tuple by nth
index is considered unidiomatic.
Due to its polymorphic nature, a tuple can almost be thought of as a record / object, which often prefer dot notation like a.fieldName
as a convention to access its field (except in language like Javascript, which treats objects like dictionaries and allows string literal access like a["fieldname"]
. The only language I'm aware of that's using indexing syntax to access a field is Lua.
Personally, I think syntax like a.(0)
tends to look better than a.0
, but this may be intentionally (or not) awkward considering in most functional languages it is ideal to pattern-match a tuple instead of accessing it by its index. Since Rust is also imperative, syntax like a.10
can be a good reminder to pattern-match or "go use a struct" already.
This decision was made in RFC 184. The Motivation section has details:
Right now accessing fields of tuples and tuple structs is incredibly painful—one must rely on pattern-matching alone to extract values. This became such a problem that twelve traits were created in the standard library
(core::tuple::Tuple*)
to make tuple value accesses easier, adding.valN()
,.refN()
, and.mutN()
methods to help this. But this is not a very nice solution—it requires the traits to be implemented in the standard library, not the language, and for those traits to be imported on use. On the whole this is not a problem, because most of the timestd::prelude::*
is imported, but this is still a hack which is not a real solution to the problem at hand. It also only supports tuples of length up to twelve, which is normally not a problem but emphasises how bad the current situation is.
The discussion in the associated pull request is also useful.
The reason for using t.2
syntax instead of t[2]
is best explained in this comment:
Indexing syntax everywhere else has a consistent type, but a tuple is heterogenous so
a[0]
anda[1]
would have different types.