Discussions in the pull request thread of RFC 3723 - Static Function Argument Unpacking led to the conclusion, that defining unpacking into arrays and tuples would be a good idea before going forward with unpacking arguments.
I've started drafting a separate RFC for that extension and I think it's ready for a vibe check. Please note that it is quite heavily littered with todos, and the text is nowhere near ready to being submitted on GitHub, yet.
I couldn't get a hold of the author for the existing Pre-RFC: Array expansion syntax, but would like to thank them for their effort, which I've drawn a lot of inspiration from.
Edit 2025-03-16: Typo fixes, more coverage for other programming langs, made RFC template todos expandable for now.
Summary
This RFC extends RFC 3723 – Static Function Argument Unpacking by adding compile-time unpacking of tuples, tuple structs, and fixed-size arrays in definitions of tuple and fixed-size array literals. The syntax defined in RFC 3723 for argument unpacking, ...expr
, is used inside the literal definition's parentheses or brackets as a shorthand for using the values of expr
as the values defined for the literal in that place.
Motivation
RFC 3723 defines an MVP of a specific feature: argument unpacking. This RFC, on the other hand, defines a separate but related feature acknowledged as a future possibility with community interest in the former RFC's comment thread. While RFC 3723 enables constructing tuple structs, it does not provide a similar mechanism for building tuples. This RFC fixes that discrepancy.
Some benefits of unpacking into tuple and array literals include, for example:
- Unpacking by hand is prone to copy-paste errors and typos, leading to syntactically correct erroneous code. For example, referring an incorrect index within the source array is easy to miss. This is exacerbated as collections containing more elements are unpacked. This RFC equips developers with a tool for avoiding that problem.
- Code becomes shorter when unpacking in literals is used, as multiple consecutive accesses to fields in a collection can be replaced by a single occurrence of unpacking. Readability and maintainability are positively affected.
- Code that previously repeated existing values already defined elsewhere, just to avoid the difficult-to-read syntax of accessing them manually, may become shorter and easier to change when switching to using these other collections by unpacking them.
Conjecture: Certain scientific and engineering fields involve code handling large arrays of numbers – so much so that languages such as MATLAB® have been created to fulfill these needs. For example, working with measurement data, digital signals, images and videos is often backed by code working on arrays. Unpacking into literals makes it more ergonomic to work with arrays in some situations, making Rust a more appealing choice for writing professional and consumer software involving any of the kinds of data listed above, such as video codecs, image manipulation software, and so on.
const
Concatenation for Arrays
Unpacking into tuple and fixed-size array literals enables a way to concatenate arrays in const
contexts – a feature with community appeal.
- How to concatenate array literals in compile time? - help - The Rust Programming Language Forum
- Concatenating arrays - help - The Rust Programming Language Forum
- Can "const fn" concatenate byte slices? - help - The Rust Programming Language Forum
- GitHub - eira-fransham/const-concat: Heinous hackery to concatenate constant strings.
- crates.io: Rust Package Registry
Initialization of Arrays Combining Specified and Default Values
This is somewhat similar to Functional Record Updates (FRU) / Struct Update Syntax.
Now:
static ARR: [u8; 255] = [0, 1, ...[0xff; 253]];
Before:
static ARR: [u8; 255] = {
let mut arr = [0xff; 255];
arr[0] = 0;
arr[1] = 1;
arr
};
(In non-const
contexts, array::from_fn
can be used)
let x: [u8; 5] = core::array::from_fn(|i| match i {
0 => 0,
1 => 1,
_ => 0xff,
});
- rust - Initialize rest of array with a default value - Stack Overflow
- syntax - How to initialize a static array with most values the same but some values different? - Stack Overflow
Conversions Between Tuples and Arrays
Unpacking into tuple and fixed-size array literals also provides a way to convert between tuples and arrays.
Guide-Level Explanation
To define fixed-size arrays and tuple literals in a way that uses the values from other such collections, these source collections can be unpacked within the definition of the new literal. This means that in such cases, the tedious alternative of accessing each item in the source collection one by one can be avoided.
TODO: Elaborate:
- "what is being unpacked": the automatic using of all items in the "what" consecutively by syntactic sugar
...
- "what is being unpacked": again as in rfc 3723, collections with sizes known during compilation time
- "where is it unpacked to": this time, instead of function arguments as in rfc 3723, into definitions of fixed-size array and tuple literals
- "when does unpacking occur": as in rfc 3723, during compilation, not run-time. This means unpacking can't panic.
Some things of note:
- Unpacking is exhaustive, i.e. all values in the source collection are unpacked.
- The items in the source are unpacked in the order in which they are in the source collection.
There are also certain limitations to this:
- Unpacking into literals works during program compilation, not when the program is run. Consequently, in this MVP, it only works on certain types. As an upside, the program cannot panic due to this, and doing this is zero cost.
Simple Concatenation Example
To define a fixed-size array literal with values from another, unpack it thus:
/// A handful of numbers at the beginning of the Fibonacci sequence
const FIB5: [u8; 5] = [0, 1, 1, 2, 3];
/// A longer sequence of Fibonacci numbers (unpacking in literal definition)
const FIB10: [u8: 10] = [...FIB5, 5, 8, 13, 21, 34];
The old, manual way without unpacking into fixed-size array literals would have been:
/// A longer sequence of Fibonacci numbers (defined manually)
const FIB10: [u8: 10] = [FIB5[0], FIB5[1], FIB5[2], FIB5[3], FIB5[4], 5, 8, 13, 21, 34];
The alternative way above is also what the code would look like after the syntactic sugar of unpacking has been desugared.
TODO: Combining Defined Values with Default Values
const LEET_ONES: [u8; 64] = [1, 3, 3, 7, ...[1; 60]];
TODO: Many Unpackings in Same Definition Example
static RGB_MASK: [u64; 640 * 480] = [
...[0xFF00007F; 640 * 160],
...[0x00FF007F; 640 * 160],
...[0x0000FF7F; 640 * 160],
];
Transpiling C and C++ to Rust
C and C++ allow specifying arrays part-way, while the remaining elements are zeroed. Efforts such as c2rust could benefit from this RFC.
E.g., the following code (same in C and C++)
void foo() {
int x[32] = {1, 2, 3, 4};
}
can now be transpiled to
fn foo() {
let mut x: [libc::c_int; 32] = [1, 2, 3, 4, ...[0; 28]];
}
TODO: Unpacking a Byte String Example
Based on example given by scottmcm:
const HELLO_C: [u8; 14] = [...*b"Hello, world!", b'\0'];
or, further
TODO: check for inspiration: Magic number (programming) - Wikipedia
const FILE: [u8; 140] = [...*b"MZ", b'\0']; // ... TODO
Conversions between Arrays and Tuples
You can convert an array into a tuple:
let arr = ["cat", "dog", "rat", "bat"];
let my_tup = (...arr);
assert_eq!(my_tup, ("cat", "dog", "rat", "bat"));
Itertools has collect_tuple
.
And a tuple into an array:
let tup = (1.00, 1.25, 1.50, 1.75, 2.00);
let my_arr = [...tup];
assert_eq!(my_arr, [1.00, 1.25, 1.50, 1.75, 2.00]);
Unpacking the Output of Const Functions
Consider the following program with the definition for WITH_PADDING
omitted:
const fn frobnicate<const N: usize>(arr_in: [u8; N]) -> [u8; N] {
let mut arr_out = arr_in;
let mut i = 0;
while i < N {
arr_out[i] <<= 2;
i += 1;
}
arr_out
}
// … definition of WITH_PADDING cut out …
fn main() {
println!("{:#?}", WITH_PADDING);
}
WITH_PADDING
can be defined thus, unpacking the results of the const
function frobnicate
in the middle of the array:
const WITH_PADDING: [u8; 5] = [0, ...frobnicate([1, 2, 3]), 0];
Before unpacking into array and tuple literals was possible, one or the other of following would have been needed:
const WITH_PADDING: [u8; 5] = [
0,
frobnicate([1, 2, 3])[0],
frobnicate([1, 2, 3])[1],
frobnicate([1, 2, 3])[2],
0,
];
const WITH_PADDING: [u8; 5] = {
let mut x = [0; 5];
let result = frobnicate([1, 2, 3]);
x[1] = result[0];
x[2] = result[1];
x[3] = result[2];
x
};
Reference-Level Explanation
TODO: RFC template points
[!TIP]
This is the technical portion of the RFC. Explain the design in sufficient detail that:
- Its interaction with other features is clear.
- It is reasonably clear how the feature would be implemented.
- Corner cases are dissected by example.
The section should return to the examples given in the previous section, and explain more fully how the detailed proposal makes those examples work.
The operator ...
operates on the expression it is prefixed to, expr
for short, by unpacking the operand.
Requirements for Unpackable expr
s
What can be unpacked?
Anything that cannot be unpacked by hand, cannot be unpacked automatically either.
The expr
s that can be unpacked have the following requirements:
- It has a collection type, whose items can be accessed, in order, with indices starting from 0 and ending at length - 1.
- Its size is known during program compilation.
For example, the following evaluate into possible operands for unpacking at compile time:
- The following literals:
- Array expressions.
- Tuple expressions.
- Byte string literals.
- Fixed-size arrays.
- Tuples.
- Tuple structs.
As a corollary of the above rules, this MVP of unpacking into literals does not cause run-time panics.
Syntactic Sugar
When does unpacking happen?
Unpacking into literal definitions is syntactic sugar.
In a fixed-size array or tuple literal definition context, where n denotes the length/size/number of items in expr
, ...expr
desugars into consecutive comma-separated form of:
- when unpacking an array:
expr[0]
,expr[1]
, …,expr[n - 1]
, or - when unpacking a tuple (struct):
expr.0
,expr.1
, …,expr.(n - 1)
.
Basically, the above desugared forms are exactly the same as unpacking them by hand would be.
Syntax Details
Where can unpacking be used?
The exact same form of prefixing the expression that is unpacked with an operator, when in a specific context, with the same operator symbol, is selected as in RFC 3723. In short, this means using the three ASCII dot ellipsis ...
as the operator.
The contexts where this applies are array expressions and tuple expressions.
Array expressions are changed by including ...Expression
as an alternative in ArrayElements for both of the two comma-separated _Expression_s .
Tuple expressions are changed by including ...Expression
as an alternative in TupleElements for both of the two comma-separated _Expression_s .
Building tuple structs this way is already supported by changes introduced in RFC 3723, since those are built using a constructor function that can be called using argument unpacking.
Errors
No new errors arise from the use of unpacking into tuple and array literals. However, existing errors may take the syntactic sugar into account, if necessary. The erroneous situations in the subchapters below demonstrate errors already handled by the compiler, but shown in the context of the new feature.
Length Mismatch
No attempt is made as part of this feature to (type-)check the surroundings for possible constraints to how many items can be unpacked. Any compilation errors arising from attempting to unpack an incorrect-size collection follow automatically from how these errors are already handled by the compiler.
E.g., the following two will fail due to the same error – attempting to define a type of length-2 array to have a length-4 array:
let src: [i8; 4] = [123; 4];
let dst: [i8; 2] = [...src];
// ^ note difference in length
let src: [i8; 4] = [123; 4];
let dst: [i8; 2] = [src[0], src[1], src[2], src[3]];
// ^ note difference in length
Error message for latter (where the source array is manually unpacked):
error[E0308]: mismatched types
--> src/main.rs:3:24
|
3 | let dst: [i8; 2] = [src[0], src[1], src[2], src[3]];
| ------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an array with a size of 2, found one with a size of 4
| | |
| | help: consider specifying the actual array length: `4`
| expected due to this
Error message for former (with unpacking syntax):
error[E0308]: mismatched types
--> src/main.rs:3:24
|
3 | let dst: [i8; 2] = [...src];
| ------- ^^^^^^^^ expected an array with a size of 2, found one with a size of 4
| | |
| | help: consider specifying the actual array length: `4`
| expected due to this
Cyclic Definitions
Rust compiler already handles the desugared form with cyclic definitions correctly. Consequently, the following two examples where an array is defined using the other and vice versa – using unpacking or without it – will fail due to the same error:
const A: [u8; 1] = [...B];
const B: [u8; 1] = [...A];
const A: [u8; 1] = [B[0]];
const B: [u8; 1] = [A[0]];
Error message with latter (where the source array is manually unpacked):
error[E0391]: cycle detected when simplifying constant for the type system `A`
--> src/main.rs:1:1
|
1 | const A: [u8; 1] = [B[0]];
| ^^^^^^^^^^^^^^^^
|
note: ...which requires const-evaluating + checking `A`...
--> src/main.rs:1:21
|
1 | const A: [u8; 1] = [B[0]];
| ^
note: ...which requires simplifying constant for the type system `B`...
--> src/main.rs:2:1
|
2 | const B: [u8; 1] = [A[0]];
| ^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `B`...
--> src/main.rs:2:21
|
2 | const B: [u8; 1] = [A[0]];
| ^
= note: ...which again requires simplifying constant for the type system `A`, completing the cycle
= note: cycle used when running analysis passes on this crate
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Error message for former (with unpacking syntax):
error[E0391]: cycle detected when simplifying constant for the type system `A`
--> src/main.rs:1:1
|
1 | const A: [u8; 1] = [...B];
| ^^^^^^^^^^^^^^^^
|
note: ...which requires const-evaluating + checking `A`...
--> src/main.rs:1:21
|
1 | const A: [u8; 1] = [...B];
| ^
note: ...which requires simplifying constant for the type system `B`...
--> src/main.rs:2:1
|
2 | const B: [u8; 1] = [...A];
| ^^^^^^^^^^^^^^^^
note: ...which requires const-evaluating + checking `B`...
--> src/main.rs:2:21
|
2 | const B: [u8; 1] = [...A];
| ^
= note: ...which again requires simplifying constant for the type system `A`, completing the cycle
= note: cycle used when running analysis passes on this crate
= note: see https://rustc-dev-guide.rust-lang.org/overview.html#queries and https://rustc-dev-guide.rust-lang.org/query.html for more information
Lints
It is conceivable that unpacking is, by accident, used in situations the user did not intend to. Compile-time warnings are output for the following cases:
-
Defining an array wholly by unpacking just one source array, or a tuple by wholly unpacking just one source tuple:
let y = [...x];
- Lint: shorter way to express is
let y = x;
- Lint: shorter way to express is
-
Unpacking an empty source:
let src: [i8; 0] = []; let dst = [5, ...src];
- Lint: attempting to unpack a collection with zero elements has no effect.
- This is the same warning raised when unpacking a zero-length array as function arguments, as per RFC 3723.
Interactions with Tools
Rustfmt
...expr
within an array or tuple definition is formatted as if it forms an expr
.
Rustdoc
Unpacking does not lead to any new documentable items or change existing ones.
Rust-Analyzer
Support for new syntax is required.
Code actions for using unpacking may be implemented.
Cargo-semver-checks
No interactions. Unpacking relates to using APIs, not defining them.
Drawbacks
- If we want to implement a future possibility of run-time unpacking of dynamically sized types, having both the unpacking operator
...
and the range sugar..
appear close to each other may look confusing. E.g., when unpacking into an array this way:[...my_arr[idx..idx2], ...other_slice[a..b]]
. - However useful this syntactic sugar may be, it is additional syntax to maintain by the project, support in the compiler, account for when planning new features, handle by other software working with Rust code, teach in the documentation, understand by the developers, and so on.
- We may want to implement array concatenation in some other way entirely, and having the syntactic sugar of unpacking into array literals provides a duplicate feature.
Rationale and Alternatives
TODO: RFC template points
[!TIP]
- Why is this design the best in the space of possible designs?
- What other designs have been considered and what is the rationale for not choosing them?
- What is the impact of not doing this?
- If this is a language proposal, could this be done in a library or macro instead? Does the proposed change make Rust code easier or harder to read, understand, and maintain?
Use of the Same Syntax as for Argument Unpacking
Since unpacking in literal definitions is very closely related to argument unpacking, choosing a different syntax for each would most likely cause confusion. Unless the community specifically wants to diverge in syntax for these two cases, the symbol, whether it is prefixed or suffixed, etc. are best bikeshed in the comment thread of RFC 3723.
Other programming languages that have both of these features also usually use the same syntax for both.[citation needed]
Not Implementing This Feature
In some contexts, arrays can simply be concatenated with existing functions.
Implementing compile-time array concatenation is somewhat tricky, but can be done.
Const generics allow some tricks in defining arrays: Concatenating arrays - #5 by jbe - help - The Rust Programming Language Forum
It's possible to write macros to concatenate arrays: Concatenating arrays - #9 by scottmcm - help - The Rust Programming Language Forum
The above tricks can be provided by a crate.
build.rs
can also be used.
Arguably, none of the available workarounds leave a particularly developer-friendly impression, while the syntactic sugar of unpacking in array and tuple literals is intuitive and, compared to macro, build.rs
, and external crate solution, reduces compilation times.
One of the main benefits is ergonomics, and having to invoke macros has some overhead due to the additional boilerplate, while a bespoke syntax does not have this problem.
E.g., for vectors, you can .extend
.
Concatenate with +
Some languages, such as Python and Ruby, allow concatenating arrays with the infix +
operator (see examples below under Prior Art). However, even in Python ecosystem, for example in NumPy, the +
symbol between array arguments has a different meaning: it does elementwise addition of arrays of the same size. For example:
>>> arr_a = np.array([1, 2, 3])
>>> arr_b = np.array([4, 5, 6])
>>> arr_c = arr_a + arr_b
>>> arr_c
array([5, 7, 9])
MATLAB® and Octave also use infix +
for addition:
octave:1> a = [1, 2, 3]
a =
1 2 3
octave:2> b = [4, 5, 6]
b =
4 5 6
octave:3> c = a + b
c =
5 7 9
Selecting the syntax of infix +
for array concatenation in Rust could cause confusion and would conflict with possible uses of mathematical symbols for other purposes, such as elementwise addition. Moreover, focusing on concatenation does not help with the other issues unpacking provides solutions for.
Prior Art
TODO: RFC template points
[!TIP]
Discuss prior art, both the good and the bad, in relation to this proposal. A few examples of what this can include are:
- For language, library, cargo, tools, and compiler proposals: Does this feature exist in other programming languages and what experience have their community had?
- For community proposals: Is this done by some other community and what were their experiences with it?
- For other teams: What lessons can we learn from what other communities have done here?
- Papers: Are there any published papers or great posts that discuss this? If you have some relevant papers to refer to, this can serve as a more detailed theoretical background.
This section is intended to encourage you as an author to think about the lessons from other languages, provide readers of your RFC with a fuller picture. If there is no prior art, that is fine - your ideas are interesting to us whether they are brand new or if it is an adaptation from other languages.
Note that while precedent set by other languages is some motivation, it does not on its own motivate an RFC. Please also take into consideration that rust sometimes intentionally diverges from common language features.
RFC 3723 discusses the specific feature introduced in this RFC in its top Future Possibility subchapter Unpacking in Fixed-Size Array and Tuple Literals.
Past Proposals
Two major proposals, in the form of pre-RFCs by nwn on Rust Internals, lay out features similar/same to this RFC:
- Pre-RFC: Array fill syntax proposes a syntax for filling remaining parts in an array with a given expression.
- The evolution of the previous, Pre-RFC: Array expansion syntax, proposes a full-fledged syntax for a similar way of unpacking as this RFC.
TODO: This RFC (as well as RFC 3723) acts on the "mirror side" of variadics, and thus this unpacking feature is typically mentioned in variadic generics initiatives
Crates
velcro
provides an alternativevec!
macro that uses the unary..
prefix operator for unpacking collection into the vector.lit_vek
provides thevek!
macro, a drop-in replacement forstd::vec!
that also features the...arr
syntax for unpacking iterables into the vector.
Other Programming Languages and Unpacking
Several other programming languages have this same (or a very similar) feature, allowing unpacking contents of collections into other collections. Some have discussed adding such feature.
- Dart: Spread operator: https://dart.dev/language/collections#spread-operators
- JavaScript: Spread syntax (...): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
TODO: Probably others too... Find out.
Python
PEP 448: PEP 448 – Additional Unpacking Generalizations | peps.python.org
>>> a = [1, 2, 3]
>>> b = [4, 5, 6]
>>> c = [*a, *b]
>>> c
[1, 2, 3, 4, 5, 6]
Note that Python (like Ruby) allows concatenating lists with a different syntax as well (continuing from above example):
>>> d = a + b
>>> d
[1, 2, 3, 4, 5, 6]
Ruby
Splat operator in implicit array assignment: https://docs.ruby-lang.org/en/3.4/syntax/assignment_rdoc.html#label-Implicit+Array+Assignment
irb(main):001:0> a = [1, 2, 3]
=> [1, 2, 3]
irb(main):002:0> b = [4, 5, 6]
=> [4, 5, 6]
irb(main):003:0> c = [*a, *b]
=> [1, 2, 3, 4, 5, 6]
Note that Ruby (like Python) allows concatenating lists with a different syntax as well (continuing from above example):
irb(main):004:0> d = a + b
=> [1, 2, 3, 4, 5, 6]
Go
- proposal: spec: allow using spread on arrays in function calls, assignment and return statements
- proposal: Go 2: spread syntax use for slice literals
Other Programming Languages and a Subset of Features
Some languages have alternative features comprising a subset of the possibilities enabled by unpacking as described in this RFC.
C and C++
For example, both C and C++ allow initializing arrays combining selected and default zeroed values, e.g.:
// sets indices 0, 1, 2 and 3 to values below; rest to zeroes
int x[32] = {1, 2, 3, 4};
Or in a C99 extension (not supported by C++):
// sets index 4 to value below; rest to zeroes
int x[32] = { [4] = 5 };
- https://gcc.gnu.org/onlinedocs/gcc/Designated-Inits.html
- https://en.cppreference.com/w/c/language/array_initialization
D
D allows array initialization with selected and default values as well:
int[5] a = [ 1:2, 3:4 ];
assert(a == [0, 2, 0, 4, 0]);
In D, arrays can be concatenated with the binary infix ~
operator (the cat operator), e.g.:
int[] a = [1, 2];
int[] b = a ~ [3, 4];
assert(b == [1, 2, 3, 4]);
Unresolved Questions
TODO: RFC template points
[!TIP]
- What parts of the design do you expect to resolve through the RFC process before this gets merged?
- What parts of the design do you expect to resolve through the implementation of this feature before stabilization?
- What related issues do you consider out of scope for this RFC that could be addressed in the future independently of the solution that comes out of this RFC?
- Would this already support unpacking into slice definitions as well, without extra effort?
Future Possibilities
TODO: RFC template points
[!TIP]
Think about what the natural extension and evolution of your proposal would be and how it would affect the language and project as a whole in a holistic way. Try to use this section as a tool to more fully consider all possible interactions with the project and language in your proposal. Also consider how this all fits into the roadmap for the project and of the relevant sub-team.
This is also a good place to "dump ideas", if they are out of scope for the RFC you are writing but otherwise related.
If you have tried and cannot think of any future possibilities, you may simply state that you cannot think of anything.
Note that having something written down in the future-possibilities section is not a reason to accept the current or a future RFC; such notes should be in the section on motivation or rationale in this or subsequent RFCs. The section merely provides additional information.
The future possibilities are largely the same as in RFC 3723, although some of the concepts listed there can be specifically adapted into the context of unpacking into collections, which this RFC is the MVP for.
Unpacking in Standard Library Macros
Supporting unpacking of collections in standard library macro invocations, especially when creating vectors with vec![]
, is expected to be allowed when this RFC lands.
See crates velcro
and lit-vek
listed under Prior Art subchapter Crates.
Dynamic Contexts
Run-time unpacking of dynamically sized types (DST) into arrays and tuples.
The following collections matching this have a clearly defined order for the elements they hold:
- Unpacking of slices
- Unpacking of vectors
This effort would likely go hand in hand with run-time argument unpacking of DSTs.
See crates velcro
and lit-vek
listed under Prior Art subchapter Crates.
General Unpacking Operator
This effort would also likely go hand in hand with run-time argument unpacking.