Some parts of this article are likely to be out of date, though I haven’t looked in detail (I’m just going to bed). Most significantly, the recent const generics stabilisations has shifted the balance of power and may have made some alternatives nicer. I do, however, note that [T; N]’s Default implementation is still limited to N ≤ 32, for technical reasons that haven’t yet been overcome. (https://github.com/rust-lang/rust/pull/74254 is relevant info that I’ve found on a quick search—I think the trouble is that [T; N] normally requires T: Default, but [T; 0] doesn’t, and this can’t be sorted out with the minimum const generics that has shipped. An unfortunate case of designing oneself into a corner by doing things that made perfect sense at the time.)
An important one is missing from this overview: using a const. This is sometimes useful, because it does not require the type to be Copy; only that the value is a constant:
There's also an open Pull Request for adding std::array::from_fn which calls the function / closure provided N times to initialize an array. This basically brings the array-init functionality into std.
It’s a historical limitation, with the power of lifting it only very recently stabilised (in the last couple of releases). Most things that were limited to 32 when this article was written aren’t any more, though the Default trait implementation still is.
The key term to search for and read up on is const generics.
This style of limitation is still present on tuples, which have implementations for common traits for up to 12-tuples. (See the bunch of implementations around https://doc.rust-lang.org/std/cmp/trait.PartialEq.html#impl-... as an example, comparing it with the single “PartialEq<[B; N]> for [A; N]” implementation for all arrays!) This limitation will probably be sorted out eventually, but it takes time to settle on a design that satisfies everyone and is implementable.
Why not? Rust isn't limited to what Haskell does, and a lot of it's features are driven by a desire for parity with C++, which does have variadic generics.
Other languages in the same domain (like D) also have variadic generics.
You're conflating traits with generics. Generics exist without traits, and in Rust they are more closely aligned with C++ templates than anything else, since they are implemented via monomorphisation, whereas in Haskell they are not.
Regardless, Rust is not "patterned" after a single other language. It's influenced by many langauges, so it makes no sense to say that one feature is just copied straight from a specific other language.
There have been various variable arity generic discussions on the internals forum (internals.rust-lang.org), but I think it's a significantly lower priority issue than the other stuff in flight. I think we might get it in a few years.
This is my position, too. I think everyone agrees that we want to get generic tuples, but there are other higher priority issues still, so it’s mostly more a backburner project worth thinking about when designing things, and slowly working towards as good a design as we can come up with which fits in with the rest of the language (including future possibilities) as well as possible. Even in a vacuum it’s not trivial to design something both satisfactory and implementable, and when you add other language features, corners the language has been designed into, and corners you want to avoid painting the language into, it gets much harder still.
Traits are applied to types and because the length of the array is part of the type, different length arrays are different types.
It's only with the very recent launch of const generics (allowing constant values like numbers as generic type parameters) that it became possible to abstract over things like array length.
IIRC there is some stickiness about the default trait that is still blocking a const generic array initializer.
Really stuck out to me too. Does that mean that somewhere in there there are 32 different "overloads" (or whatever the right term is) of this functionality for 1 to 32 elements? It feels so out of place...
That macro implements Default, but only for arrays of size 1 to 32. It doesn't make any sense to talk about a Default array of size 0, the array is empty, there's no "default" content at all.
You can see the same verbosity for places where in some other languages you'd be able to just say all these things just work with any number of elements in a tuple, and Rust has chosen "up to 12" instead because it cannot express that.
The top half of that file is explaining to Rust's macro language how to do various things for an N-tuple, and then the bottom half call that macro for the 1 through 12 sizes.
If you really need a 16-tuple (unlikely, but...) you could copy paste the code to make one with the same features. This isn't compiler support, so you could just do it yourself.
> If you really need a 16-tuple (unlikely, but...) you could copy paste the code to make one with the same features. This isn't compiler support, so you could just do it yourself.
There's one wrinkle in that strategy in that you might run afoul of the orphan rule[1][2] where you can't write:
impl ForeignTrait for ForeignType {}
because if we did it would be brittle and changes to an external crate could make your unchanged crate stop compiling. So you're restricted to either implementing your own trait or any trait on a type you declared. To "get around this" you can use the new-type pattern[2]:
struct MyArray<T>([T; 33]);
impl ForeignTrait for ForeignType {}
My error. Good point about the orphan rule. Because I was writing about Tuples what I was talking about is actually impossible in your own code I think, because tuples are inherently foreign. Even though the code in std looks tame, you are forbidden from doing this elsewhere. Even for a hypothetical local type MyType you are forbidden from attempting:
impl ForeignTrait for (MyType,u8)
The compiler will conclude that ForeignTrait is foreign, of course which is what we expected, but (MyType,u8) is also foreign even though MyType is local and so the orphan rule forbids this definition. Shame.
It doesn't make any sense to sidestep with new type because if you didn't want a tuple you could already define whatever type you wanted and this isn't an issue anyway, whereas if you do really want a tuple then a new type isn't a substitute.
Without const generics, traits had to be implemented on every arity. Even with macros, there has to be a limit somewhere in the number of impls you allow as the implementations are explicit and eager.
It was normally implemented with macros to hide the boilerplate, but I agree that it wasn't very elegant. You can understand why many of us were very happy to finally get const generics...
- &[Elem]: A "slice" of elements, represented as a pointer and a length.
- [Elem; N]. A fixed-size array where the number of elements is known at compile time. This might be used for 2D or 3D coordinates, for example.
Array slices have always supported arbitrary lengths. But arrays with fixed compile-time sizes are represented as entirely different types: it's an error to pass a [i32; 2] to a function that wants an [i32; 3]. The size of the fixed-size array is part of the type.
And until very recently, Rust's type system didn't support defining methods on [Elem; N] that were generic over N. This is a feature that tends to be added later in many languages that support generics, not just Rust.
The reason this limitation exists is because it was decided that an array length of 0 should impl `Default` even if `T: !Default`, so `impl<T: Default, N: usize> Default for [T; N]` can't be used.
This may be possible in the future if specialization is stabilized, but for now we're stuck with that limit for `Default`.
Some parts of this article are likely to be out of date, though I haven’t looked in detail (I’m just going to bed). Most significantly, the recent const generics stabilisations has shifted the balance of power and may have made some alternatives nicer. I do, however, note that [T; N]’s Default implementation is still limited to N ≤ 32, for technical reasons that haven’t yet been overcome. (https://github.com/rust-lang/rust/pull/74254 is relevant info that I’ve found on a quick search—I think the trouble is that [T; N] normally requires T: Default, but [T; 0] doesn’t, and this can’t be sorted out with the minimum const generics that has shipped. An unfortunate case of designing oneself into a corner by doing things that made perfect sense at the time.)