RFA-638 · Case file with fixtures · Case 610 of 694 · Compiler evidence
A Trait Associated Function Call Needs a Concrete Implementation
A receiver-free trait function still belongs to an implementation. Name the concrete type with Type::method or fully qualified syntax, or make the choice an explicit generic parameter.
- 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 trait declaration was mistaken for one concrete namespace even though its receiver-free function may differ for every implementation.
- First discriminating check
- Name the implementing type directly or with fully qualified syntax, or expose that choice as a generic parameter.
A trait associated function without self resembles a namespace function, but each implementation may provide a different result. Calling the trait name alone does not choose among them. The failing fixture calls Identifier::zero() and receives E0790.
No receiver means no inferred implementation
For value.method(), the receiver type helps method resolution select an implementation. A receiver-free call has no such value. The return type may also be identical across implementations, so assigning it to u64 cannot reveal whether UserId or another ID policy was intended.
The official E0790 explanation asks for a concrete implementation type. The repaired fixture uses <UserId as Identifier>::zero(), making both type and trait explicit.
The short form UserId::zero() can work when trait lookup is unambiguous and the trait is in scope. I use fully qualified syntax when several traits define the same name or when clarity at a boundary matters.
Fully qualified syntax states the dispatch choice
The Book explains fully qualified syntax. The general form <Type as Trait>::function(arguments) identifies exactly which implementation contributes the associated item.
This is static dispatch. It does not create a trait object or perform runtime implementation search. Rust resolves the implementation during compilation.
I prefer the explicit form in macro output because it avoids depending on imports and future inherent methods. In ordinary local code, the shorter type-qualified form may read better.
Generic code can let the caller choose
If a function should work for any identifier policy, I introduce a type parameter I: Identifier and call I::zero(). The caller or surrounding type then selects I. The generic signature makes that dependency part of the API.
If only one concrete type is ever meaningful, putting the function on a trait may be unnecessary abstraction. An inherent UserId::zero() communicates single ownership without requiring trait resolution.
Traits are useful when multiple types share a genuine contract and generic algorithms benefit. I avoid creating one solely to mock a receiver-free constructor; constructors can often be injected as values or closures in tests.
Associated constants may express values better
A zero identifier that performs no computation may be an associated constant rather than a function. const ZERO: u64 still needs an implementation type, but it signals that evaluation has no operation or failure.
If construction validates invariants or may fail, a function returning Result is more honest. Names such as default, zero, new, and empty should match domain meaning. Not every identifier accepts zero as valid.
I use this compiler error as a prompt to review that semantic contract, not merely insert a type name.
Macros and prelude changes can create ambiguity
Generated code may know a trait but omit the implementing type that was available in the input schema. The macro should carry type identity through its internal representation and emit qualified calls.
Adding an inherent function with the same name or importing another trait can change short-form resolution. Fully qualified calls protect critical generated behaviour. Compile tests should include multiple candidate traits and types so the generator cannot rely on accidental uniqueness.
The Reference describes disambiguating function calls, including paths that identify trait implementations.
I also check whether the associated function should accept a receiver after all. If behaviour depends on configuration already stored in a value, &self lets method resolution and ownership state the dependency naturally. Keeping it receiver-free while reaching into global configuration makes the missing implementation only one of several hidden inputs. A small explicit value often gives both the compiler and the reader a better selection mechanism.
My E0790 checklist
- Is a receiver-free associated function called through the trait name alone?
- Which concrete implementing type owns the intended behaviour?
- Is
Type::functionunambiguous, or should fully qualified syntax be used? - Should a generic type parameter let the caller choose the implementation?
- Is a trait useful here, or would one inherent function state ownership better?
- Would an associated constant represent a pure value more honestly?
- Can zero, default, or empty violate a domain invariant?
- Does generated code retain and emit concrete implementation identity?
The core principle is that a trait declares a family of possible implementations; it is not one implementation. E0790 demands the missing choice. I state it through a concrete type, a generic parameter, or a simpler inherent API.