RFA-422 · Case file with fixtures · Case 394 of 694 · Compiler evidence
A Trait Associated Function Cannot Become an Instance Method
Receiver presence selects between an associated function and a method. Trait implementations must preserve that choice; add a receiver to the trait when object state is part of the operation, or keep the implementation static.
- 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
- Receiver presence distinguishes an associated function from a method and changes what identity and access capability callers must supply, so implementations cannot make that choice independently.
- First discriminating check
- Decide whether the operation belongs to a concrete instance, then make the receiver form match in the trait and every implementation.
I declared a trait operation as fn flush() and later wrote an implementation that needed &self. Rust did not treat the receiver as an implementation detail. It rejected the impl with E0185.
The failing fixture demonstrates only this difference. Flush::flush has no receiver; Sink implements fn flush(&self). The diagnostic says the impl uses &self although the trait method does not.
Receiver presence changes the kind of operation
A function declared inside a trait without self is an associated function. Callers select it through a type:
Sink::flush();
A function with self, &self, &mut self, or another supported receiver is a method. It can use instance state and is called from a value:
sink.flush();
These are different call contracts, not two spellings of the same function. The E0185 explanation requires the trait and impl to make the same choice.
Keep it associated when no instance is needed
The repaired fixture removes &self from the implementation and calls Sink::flush(). This fits operations that construct values, return type-level metadata, or operate entirely from arguments.
Examples include parsers such as Type::from_bytes, constants exposed through a function, or factories. There is no hidden instance from which Rust could obtain configuration.
An associated function on a trait can still use Self in its return type or bounds. That does not give it a receiver.
Add a receiver when state is essential
If every sink flushes its own buffered state, the trait probably should declare fn flush(&mut self) or fn flush(&self) depending on the mutation model. I change the trait and all implementations together.
Choosing between shared and mutable receivers is an API decision. &mut self statically prevents concurrent or overlapping calls through ordinary references. &self permits shared calls but state changes then need atomics, locks, cells, or no mutation.
I do not select &self only because it is convenient for callers. The receiver should tell the truth about access.
There is no automatic instance during static dispatch
For Sink::flush(), which Sink value should Rust pass? There may be zero, one, or millions of instances. Creating a default would require an unadvertised Default bound and would flush the wrong state. Looking for a global instance would introduce hidden identity.
Requiring an explicit receiver prevents all of these invented policies.
Object dispatch makes the distinction more visible
Methods with suitable receivers can be dispatched through dyn Trait. Receiver-free functions normally cannot be called from a trait object because there is no instance dispatch needed to select them and often no concrete Self named at the call site.
Such functions commonly use where Self: Sized, keeping them available on concrete implementors while allowing the rest of the trait to remain dyn-compatible. This connects to the Atlas cases on receiver-free functions and trait objects, but E0185 happens earlier: the impl does not match its trait at all.
A context parameter can be clearer than receiver state
Sometimes an implementation adds &self only to reach configuration. Another design passes a context argument explicitly:
trait Flush {
fn flush(context: &FlushContext);
}
This remains associated and makes dependencies visible. It is useful when the operation belongs to the type but not to a persistent object. If identity and evolving state matter, a method remains the clearer model.
Generated implementations need signature tests
Macros can obscure whether a receiver was generated. When E0185 originates in expansion, I inspect the expanded or source template signature rather than patching one output. A macro parameter such as receiver = mutable can make the intended choice explicit.
The Reference on methods defines methods by their self parameter. Names and placement inside a trait do not make every associated item a method.
My questions when E0185 appears
- Does the operation need one object's identity or state?
- Should state access be shared or exclusive?
- Is this really a factory or type-level operation?
- Would an explicit context parameter express the dependency better?
- Must the trait support dynamic dispatch?
- Did a macro generate the wrong receiver form?
The core principle is that a receiver is capability and identity. Adding &self asks the caller to supply an object and gives the body access to it. An implementation cannot add that requirement behind a receiver-free trait promise; the abstraction must choose the operation's owner explicitly.