What is the intent, meaning behind this switch from () in tuples left and right of the = character to []
left and right of the = character in arrays?
Okay, right of the = the []
designate and create the array.
But up to this point in the book () where used generically left and right of the = (like in println!, main,)
So why not use () generically left side of the creation of the array (which is done right side of the =.
Is []
right side of the = not enough to create an array? In other words now we have 2 ways to group more than one parameter and argument, respctively, together via () and []
on the left side of the logic = character, not?
Your question isn’t 100% clear. Your title references the book, however the questions seems to be purely about Rust’s language design. You talk about parentheses pairs ()
/[]
in terms of syntax (different sides of the =
-sign, mentioning println!
and main
), and you also mention arrays and tuples and talk about those types.
Let me assume that your main question might be simply: why are both tuples and arrays a thing (and why are they different / how are they different)?
If you mean something else, try to be more explicit. E.g. the question “why do arrays and tuples look different syntactically”, then the answer is “different things need different syntax; if they looked the same, you couldn’t tell them apart”. Or if you want to go deeper about possible syntax on the left-hand-side vs. the right-hand-side of an =
-sign, we’d have to talk about patterns.
Now, why are both tuples and arrays a thing (and why are they different / how are they different)? The main difference is: arrays are homogeneous and tuples can be heterogeneous.
- Tuples being heterogeneous means that a single tuple can contain values of different type. You can have
(42, "hello", true)
of type(i32, &str, bool)
- Arrays being homogeneous means that an array with multiple elements can only contain elements of the same type. So something like
[1, 2, 3]
works, and its type is[i32; 3]
, but[1, 2, true]
won’t work.
This has two kinds of implications.
- Tuples being heterogeneous means that they are more flexible. This means you can use tuples in more settings than arrays. You can use tuples for grouping together values of different types or values of the same type, however you like; while arrays only support values of the same type.
- Arrays always being heterogeneous allows you to do a few things with arrays which tuples don’t support.
- Arrays have a guaranteed specified layout in memory while tuples don’t. This allows you to create slice(-reference)s from arrays. You can turn
&[u32; 10]
or&[u32; 20]
into&[u32]
, and pass this to a function that only handles the length of the array/slice at run-time. - Array types are shorter to write. You don’t have to write something as lengthy as
(i32, i32, i32, i32, i32, i32, i32, i32, i32, i32)
to work with a 10-element array; instead you can write[i32; 10]
. - Arrays support indexing, so you can get the element number 10 of an array like
a[10]
or the one at indexi
likea[i]
(you can’t index tuples in the same way; there’s numbered fields likex.10
, but those don’t work e.g. with an index stored in a variablei
). - You can use const generics to write code that works with arrays. Something like
fn foo<const N: usize>(array: [u32; N])
can accept an array of any length by-value.
- Arrays have a guaranteed specified layout in memory while tuples don’t. This allows you to create slice(-reference)s from arrays. You can turn
I just want to note that while everything else describes fundamental differences between tuples and arrays that one is just an accident of history: year ago it was not possible to write a function which accepts array of arbitrary size and if we are lucky five years from now we would have the ability to work with arbitrarily-sized tuples, too.
C++ gained the ability to handle arbitrarily-sized arrays in 1998 and then tuples in 2011, that's 13 years between these two moments. Hopefully Rust wouldn't need that much time.
The difference between arrays of arbitrary length and tuples of arbitrary arity is that there are useful things you can do with an array because you know all its elements are the same type. The things you can do with heterogeneous tuples of arbitrary size are much more limited, and homogeneous tuples are redundant with arrays.
C++ std::tuple
isn't really comparable in this respect because in order to generalize over tuple arity you must write a variadic template which is checked at instantiation, not declaration - in Rust terms, not much more than a macro. But Rust macros already support variadic tuples! The things for which we have macros with fixed limits are the parts that attempt to interact with the type system (implementing traits, mostly). The abstraction level of templates is intermediate between Rust macros and "proper" generics (C macros being off the other end of the scale) so to use arbitrary tuples with Rust's checked generics instead of C++'s rather loosey-goosey template system would actually require a fair amount of innovation. This doesn't mean I'm opposed to the idea; I just don't think it's as simple as "C++ has it, so we're bound to get it eventually" nor "it works for arrays, so we might as well add variadic tuples too".
To bloch: please ignore, you would know terms only after you would finish book.
But that reasoning everything in C++ is just a macro. Because all templates are mostly checked for validity after an instantiation. Certain errors are detected during declaration, though. Simple example: it's syntactically valid but compiler detected error there because some minimal testing is done early.
Zero innovation, lots of implementation, actually. Yes, you would need type packs, parameter packs and some syntax to work with them, would need to think about where all these pesky ...
should go, but it's pretty boring, actually. Boring doesn't mean it's easy to implement, of course.
If you want to see how they work in more formal form look on Haskell and it's GADTs.
Yes, they are somewhat tricky to implement in the compiler, but there are nothing tricky with their use by programmer.
No, this is not my question. I read the book in e-format on my FireHD8 device. I am reffering to the p. 42, chapt. 3, subchapt. "The Array Type".
In the code example:
let a: [i32; 5] = [1, 2, 3, 4, 5]
let is getting []
all of a sudden, prior to this let only got ()
question is, isn't [1, 2, 3, 4, 5] enough to specify the array, is let a: [i32; 5] an arabesque, a stylistic inconsitency, redundancy?
Following the grammatical logic so far I would've expected this:
let a: (i32; 5) = [1, 2, 3, 4, 5]
My question was about why []
left of the expression:
let a: [i32; 5] = [1, 2, 3, 4, 5]
It's a grammatical question. The []
on the left side seems to be not consistent.
The Rust generics system can do some pretty amazing things with lisp-style lists of 2-tuples, e.g. (1, (true, (3, ())))
. When I want to write variadic tuple code, in fact, I tend to reach for a macro that transforms normal tuples into and out of this form, and then write the rest of the implementation with normal generic code.
The limitation of this approach is that the tuple length is limited to whatever the longest macro implementation is— Adding special trait implementations to the compiler to do these conversion for arbitrary-length tuples would go a long way.
[1, 2, 3, 4, 5]
is certainly enough to specify the array, and type annotations can be omitted on let statements when there is no ambiguity so this works:
let a = [1, 2, 3, 4, 5];
The [i32; 5]
is just writing out the type of the array, which brings us to the second part of your question:
The use of []
for the type of arrays is an intentional stylistic choice to make it consistent with the [1, 2, 3, 4, 5]
syntax for defining arrays and create a visual distinction from tuples. I wouldn't say it's an inconsistency, it would actually be more surprising to see ()
there since that is associated with tuples and function calls.
Okay, I see. Thanks for giving more details. The syntax of let
-statements actually contains three parts. The basic construction is
let PATTERN: TYPE = EXPRESSION;
Each of the three components, i.e. patterns, types, and expressions, have their own dedicated syntax. Each of them can also contain various kinds of brackets, and they’re often related. For example a tuple expression like (true, "hello")
has a type (bool, &str)
and can be assigned to a pattern (x, y)
. This would look like
let (x, y): (bool, &str) = (true, "hello");
For arrays, there’s a similar uniformity of using []
-brackets for all three, types, expressions and patterns; but it’s also a bit different. Expressions can be a listing of individual values like [1, 2, 3]
or [true, true, true, true]
, or a syntax that lists a single item and a number of repetition, e.v. [true; 4]
which would be equivalent to [true, true, true, true]
. Patterns only support the listing-style with commas; while array types are more similar looking to the repetition-style expressions, like the type [i32; 5]
in your example, the type of 5-element arrays of i32
s.
Now, after this general explanation let me try to address your individual questions.
This may be the first time you’re seeing square brackets used in this position, but that doesn’t have to mean much. The code example you cite is the place that introduces the syntax of array-types, so it’s unsurprising that it’s your first encounter of []
s in the type-annotation of a let expression.
The type annotation here is indeed somewhat redundant. You can write
let a = [1, 2, 3, 4, 5];
too, which is pretty much equivalent. In general, let expressions can look like the
let PATTERN: TYPE = EXPRESSION;
that I’ve mentioned before, or like
let PATTERN = EXPRESSION;
i.e. the : TYPE
part is (syntactically) optional. (Note that a single variable name is a valid pattern, too, so let a = [1, 2, 3, 4, 5];
fits this scheme by using a
as the PATTERN
.)
The type annotation is optional because Rust features type inference. It can infer the type of [1, 2, 3, 4, 5]
to be [i32; 5]
, which is why you don’t have to write it. It’s just an option to be more explicit, or have the compiler introduce a sanity-check. It can also be useful if the type is otherwise underspecified. And in fact, it is somewhat underspecified here. Number literals like 1
, 2
, or 3
, in Rust can be used for all the primitive integer types, so types like u8
, i16
, usize
or i32
, etc… The type i32
is used by default if there’s not enough typing information in the code; but you can be explicit and inform the compiler that you want a different type. E.g. if you write
let a: [u8; 5] = [1, 2, 3, 4, 5];
then the literals 1
, 2
, etc are forced to be of type u8
; in this sense the : [i32; 5]
wasn’t fully redundant.
Also – as already mentioned – this part of the book aims to introduce the syntax [i32; 5]
to you; so it makes sense to use some practically redundant type annotation in order to create a code example that shows the syntax to you.
This would definitely have been a valid approach, too; Rust was designed to use [i32; 5]
instead here, but such syntactical decisions are somewhat arbitrary. It could’ve also looked like
let a: Array<i32, 5> = [1, 2, 3, 4, 5];
and not used any special syntax at all. In fact, you can write something like this if you want in Rust today, if you define a type synonym
type Array<T, const N: usize> = [T; N];
fn main() {
let a: Array<i32, 5> = [1, 2, 3, 4, 5];
}
Note that the [T; N]
-style syntax can also appear everywhere else where types are (syntactically) expected, e.g. in function signatures
fn foo(x: [i32; 5]) { /* ..... */ }
or
fn bar() -> [i32; 5] {
[1, 2, 3, 4, 5]
}
or in more complex types, e.g. an array of arrays
fn baz2() -> [[i32; 5]; 2] {
[
[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
]
}
or an optional array
fn qux(x: Option<[i32; 5]>) { /* ..... */ }
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.