RFA-385 · Case file with fixtures · Case 357 of 694 · Compiler evidence
Two Rust Traits With the Same Method Need Explicit Selection
A receiver can satisfy several same-named trait methods. Rust will not guess from return values or implementation order; select the intended trait with Trait::method(&value) or fully qualified syntax.
- 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
- The receiver type satisfies both trait candidates, while method-call syntax contains no information selecting which trait's behavior the caller intends.
- First discriminating check
- Read every candidate in the diagnostic and use trait-qualified or fully qualified syntax at the semantic choice point.
I once implemented two small traits on the same type. Both exposed label(&self). The call item.label() looked obvious to a human reading nearby code, but both methods were valid candidates and Rust returned E0034.
The failing fixture uses Left and Right. Their defaults return different strings, so guessing would change behavior.
Method-call syntax performs a candidate search
For receiver.method(args), Rust examines the receiver type, dereference and borrow possibilities, inherent methods, and visible trait methods under the language's lookup rules. More than one trait candidate can survive that search.
The method name alone does not carry a namespace qualifier. The return context in the fixture is also identical, and both receivers are &Item. There is no type fact that makes one implementation preferable.
The E0034 explanation calls this “multiple applicable items in scope.” I treat the candidate notes as the useful part of the diagnostic: they list exactly which meanings are competing.
Select the trait at the call site
The repaired fixture demonstrates two forms:
Left::label(&Item);
<Item as Right>::label(&Item);
The first names the trait and lets the receiver determine its implementation. The fully qualified form names both the implementing type and trait. I use the shorter trait-qualified form when it is unambiguous and the full form when generic code or associated items need maximum precision.
This is not a workaround. It records a semantic choice that the original expression omitted.
Import removal can hide the design conflict
Removing one trait from scope may make method syntax compile. That is appropriate if the trait was imported accidentally. It is fragile if both traits are legitimate capabilities and another import later restores the ambiguity.
For important behavior I prefer an explicit trait name even when only one candidate is currently visible. It survives refactors and tells reviewers whether “left” or “right” semantics were intended.
Glob imports make collisions easier to introduce. I use narrow imports around extension traits with common method names such as read, write, format, or convert.
Inherent methods normally provide a stronger local meaning
An inherent method belongs directly to the type and often wins lookup before trait alternatives. Adding one can therefore change which code a previously compiling method call invokes.
When a trait's semantics must be guaranteed, qualified syntax avoids dependence on lookup priority. This matters in generic libraries and during API evolution, where a dependency can add methods without editing the call site.
I do not add forwarding inherent methods only to suppress E0034 unless the type truly wants one canonical public operation.
Return-type expectations do not provide overload resolution
Developers coming from languages with broad overloading may expect assignment context to select a method by return type. Rust's trait method lookup does not become a general “choose whichever result fits” system.
Even if Left::label returned &str and another candidate returned a custom label, relying on context for meaning would make inference changes alter behavior. Explicit qualification keeps trait selection stable.
If the traits represent the same conceptual operation, I may redesign names to reduce collisions. If they represent genuinely different viewpoints, keeping both names and qualifying calls is clear.
Generic bounds can create the same ambiguity
Inside fn show<T: Left + Right>(value: &T), both methods are guaranteed by bounds. The generic function should say Left::label(value) or <T as Right>::label(value).
This makes the choice part of the generic algorithm instead of an accident of a concrete implementation. Additional downstream impls cannot change it.
Macros also benefit from fully qualified calls. A macro expanded in another module may see different traits in scope. Qualification makes resolution less dependent on the caller's imports.
My diagnostic workflow is mechanical
I read all candidates, check whether an inherent method is involved, identify the domain behavior needed, and qualify that trait. If the choice is arbitrary, the API has not expressed enough meaning yet.
Tests assert both explicit calls in the fixture. This proves that ambiguity came from selection, not from either implementation being invalid.
The core principle is that capability overlap does not imply priority. A type can implement several traits with the same vocabulary, and each implementation remains valid. E0034 asks the caller to name which contract governs this operation. That name is valuable documentation, especially when the methods happen to have identical signatures.