RFA-442 · Case file with fixtures · Case 414 of 694 · Compiler evidence
A Tuple Variant Pattern Must Use the Right Number of Fields
Tuple-variant patterns mirror positional constructor shape. Bind or explicitly ignore every field, use .. for an intentional remainder, and reconsider named fields when positions are easy to confuse or evolve.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A tuple-variant pattern mirrors the positional product declared by its constructor, so omitted positions must be represented explicitly.
- First discriminating check
- Compare the pattern arity with the variant declaration, then bind each value, use underscore, or use a deliberate rest pattern.
I changed an enum variant from one coordinate to two and missed one pattern. Rust reported E0023 because Event::Move(x) no longer matched the declared tuple shape Move(i32, i32).
The failing fixture shows the constructor and pattern close together. The compiler identifies the expected and supplied field counts.
Tuple variants are positional products
A tuple variant carries a fixed ordered list of fields. Its pattern mirrors that list:
Event::Move(x, y)
The E0023 explanation requires a subpattern for each field. Rust cannot guess whether a lone x means the first coordinate, a tuple containing both, or an instruction to ignore the rest.
Explicit positions make ownership and borrowing apply to each component predictably.
Bind every field when every value matters
The repaired fixture destructures both coordinates and asserts the pair. This is strongest when each field drives behaviour.
I use role names rather than a and b in production. Positional types already hide labels at construction; clear local names recover some meaning at use sites.
Ignore one field with underscore
If only the first value matters:
Event::Move(x, _)
The wildcard matches one field without binding it. It also avoids moving or borrowing that ignored value merely to name it.
Naming _y is different: it creates a binding, suppresses the ordinary unused warning by convention, and follows normal move or copy rules. I choose _ when the value is genuinely not needed.
Ignore a remainder with ..
Tuple and tuple-struct patterns can use one rest pattern:
Event::Many(first, .., last)
.. stands for zero or more remaining positions. It can make a pattern tolerant of fields I deliberately do not inspect.
For a public enum in my own crate, this tolerance may hide semantic changes. Adding a tuple field is usually a breaking representation and pattern change anyway, so I review whether a rest pattern is still correct.
Named fields can make evolving records clearer
When positions are easily swapped, a struct variant may communicate better:
Event::Move { x: i32, y: i32 }
Patterns then name fields and can use .. for unmentioned ones. This does not make adding fields automatically non-breaking across all public uses, but it reduces coordinate-order mistakes.
I prefer tuple variants for small conventional shapes and named variants when fields have independent domain roles.
Nested tuple fields need another pattern layer
If a variant contains one tuple field, Event::Move((x, y)) has one outer field whose type is a pair. That is different from Event::Move(x, y), which has two outer fields.
E0023 often exposes this distinction after a refactor. I check the enum declaration rather than counting commas only at the match site.
Type aliases can also hide a tuple inside one field without changing the outer arity.
Ownership follows the subpatterns
Destructuring a non-Copy payload by value may move it. Borrowing the scrutinee or using binding modes changes what remains usable after the match.
Fixing field count can therefore reveal a second ownership error when a newly bound field moves part of the value. If the field is irrelevant, _ avoids the binding. If it is needed without ownership, I match a reference or use an explicit borrowing pattern suited to the edition.
Generated protocols need shape checks
Code generators and macros can drift when a schema changes arity. I compile generated matches as part of the schema update and include a fixture for every variant shape.
The pattern Reference defines tuple-struct and tuple-variant syntax. Generated code should follow the declaration, not a separately maintained count.
My E0023 checklist
When a tuple variant grows often, I treat E0023 as a prompt to review the representation rather than only add another underscore. Positional fields are compact when their order is obvious and stable. Once reviewers must remember whether the third value is a retry count, timestamp, or code, a struct-like variant can make later changes safer. This does not mean every tuple variant is wrong. It means the failure exposes a coupling between every destructuring site and the constructor's arity. I decide whether that coupling is useful strictness or recurring friction before applying the smallest compiling edit.
- How many fields does the variant actually declare?
- Is one field itself a tuple?
- Which positions need names,
_, or..? - Will new bindings move non-Copy data?
- Would named fields reduce position mistakes?
- Did a macro or generated schema retain an old shape?
- Should a rest pattern tolerate future internal changes?
The core principle is that patterns are structural mirrors. A tuple variant defines an exact outer arity and order. I reflect that shape explicitly, ignore fields deliberately, and change the data form when positional meaning has become too fragile.