RFA-492 · Case file with fixtures · Case 464 of 694 · Compiler evidence
A Rust Trait Method Impl Must Keep the Function Parameter List
Trait dispatch relies on one shared call shape. Every implementation must accept the same number of function parameters, including the receiver, even when a particular implementation does not need one input.
- 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
- Trait calls use one shared arity across implementations, and an implementation may ignore an input but cannot remove it from the callable contract.
- First discriminating check
- Count the receiver and ordinary parameters on both declarations before comparing their types, then restore or explicitly ignore every required input.
I declared Window::contains(&self, value: u32) and then wrote an implementation that accepted only &self. Rust emitted E0050 because the implementation no longer had the call shape promised by the trait.
The failing fixture shows a tempting mistake. My Positive implementation could return true without reading the value, but callers still pass a value through the trait interface. Ignoring an argument is allowed; deleting it is not.
Parameter count belongs to the shared contract
The official E0050 page counts the receiver as a function parameter. Therefore &self plus one ordinary input means two parameters in the declaration.
A generic caller compiles one expression such as window.contains(value) for any valid implementor. Dynamic dispatch similarly uses a compatible method slot. Rust cannot make one implementation consume fewer call arguments simply because its body does less work.
Names do not need to match, but the signature shape must.
An unused parameter stays in the implementation
The repaired fixture restores value: u32 and uses it. If an implementation truly ignores the input, I write _value: u32 or _ in an accepted parameter pattern where appropriate. This documents the intentional non-use while preserving the interface.
I do not add meaningless branching merely to silence an unused-variable warning. The underscore is the honest signal. The contract requires accepting the value, not necessarily consulting it.
Count before comparing types
E0050 is about arity. Once the count matches, Rust may report E0053 or another compatibility error for parameter types, receiver mutability, ABI-related details, or return type.
I debug signatures in layers: item name, item kind, generic parameters, function parameters, receiver form, types, lifetimes, and bounds. This prevents one large diagnostic from becoming an unstructured guessing exercise.
It also helps during code review because each difference has an API meaning.
Receivers are not invisible
Rust's method syntax makes &self feel special, yet it remains an input. fn check(&self, x: u8) and fn check(x: u8) do not have the same parameter count or invocation model.
Related receiver mismatches may receive E0185 or E0186. If the receiver is present in both declarations but one uses &mut self, self, or another incompatible type, I inspect the full signature rather than focusing only on the count.
The method-call sugar does not erase these distinctions.
Trait changes propagate to every implementation
Adding a new parameter to a public trait method breaks existing implementations and call sites. Even a parameter with a type default elsewhere cannot be omitted from a function call unless the API provides another mechanism.
When evolving a trait, I may add a new method with a default body that forwards to the old one, introduce a request struct designed for future fields, or create a second trait version. The right choice depends on compatibility promises.
Blindly updating every impl with an ignored parameter can hide that some implementations need new semantics.
Macros can duplicate the drift
A procedural macro may generate the method signature from its own model. If a trait adds one input, dozens of expanded impls can fail with E0050.
I fix the generator and test several representative expansions instead of editing generated output. For declarative macros, I check every arm because only one optional-input branch may have the stale parameter list.
Small compile fixtures make the expected arity visible without depending on a large application build.
Adapters should translate before the trait boundary
Sometimes my concrete backend naturally needs fewer inputs than the shared trait. I keep the full trait method and delegate to a smaller private helper. The adapter accepts the public argument list, validates or deliberately ignores what this backend does not need, then calls the helper. This preserves substitutability while keeping backend code simple.
The reverse is also important. If one backend needs an extra input, it cannot add that parameter only to its impl method. I provide the dependency through the implementing value, a request object, or another capability whose requirement is visible to callers.
My E0050 checklist
- How many parameters are in the trait declaration, including
self? - How many are in the implementation?
- Was an apparently unused input deleted?
- Is the receiver present on both sides?
- After fixing arity, do parameter and return types match?
- Did the trait change in the resolved dependency version?
- Is a macro emitting an older signature?
- Does a new request type offer a safer evolution path than more parameters?
The core principle is that trait users call one shared function shape. Each implementation may use the inputs differently, but it must accept every parameter that the contract promises callers can provide.