RFA-376 · Case file with fixtures · Case 348 of 694 · Compiler evidence
impl Trait Is Not a Local Variable Type Placeholder in Rust
Stable impl Trait introduces an opaque type only in supported function signature positions. For a local, use inference, name the concrete type, or choose Box<dyn Trait> when several runtime types must share one slot.
- 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
- Stable impl Trait introduces opaque types only in supported function argument and return positions, not as a general placeholder in local type annotations.
- First discriminating check
- Remove the local annotation to use inference, name the concrete type, or choose a trait object when runtime type erasure is actually required.
I once wrote let values: impl Iterator<Item = u8> = 0..3; because I wanted to name only the interface and let Rust keep the concrete adapter private. The syntax looked natural after using -> impl Iterator in functions. Rust answered with E0562.
The failing fixture shows that impl Trait is not accepted as the type of a local variable binding.
impl Trait is an opaque-type feature, not a wildcard
The impl Trait Reference defines specific positions where the syntax has meaning. In a return type, the function chooses one hidden concrete type satisfying the bounds. Callers cannot name that type, but the compiler still knows one exact layout.
In an argument position, impl Trait is closely related to a generic type parameter chosen by the caller. These two positions have different ownership of the hidden type choice, but both are part of a function signature.
A local annotation does not introduce either contract. Reading impl Iterator as “whatever type this initializer has” makes it sound like an inference placeholder, yet _ already serves that role in supported inference contexts.
The E0562 explanation reports that impl Trait is allowed in function argument and return types, not variable-binding types.
Inference is usually the direct repair
The first half of the repaired fixture simply writes let inferred = 0_u8..3;. The compiler infers Range<u8>, and the variable can use every method available for that concrete type.
If I want a check on the iterator's item type, the later consumer often supplies it: sum::<u8>(), a helper accepting Iterator<Item = u8>, or a closure boundary. I avoid annotating locals merely to restate what the initializer makes obvious.
I can also name the concrete type, such as std::ops::Range<u8>, when that improves diagnostics or documents a dependency. The tradeoff is that changing adapter composition may require changing the annotation.
A trait object solves a different problem
The repaired fixture also shows Box<dyn Iterator<Item = u8>>. This is runtime type erasure. The box has one known outer representation and uses dynamic dispatch to call iterator methods on the hidden value.
I choose it when different concrete iterator types must occupy the same variable or collection, or when a public boundary intentionally erases implementations. It can introduce allocation and indirect calls, so it is not the automatic replacement for a rejected local impl Trait.
For two branches with different adapters, alternatives include moving the branch before the adapter, using an enum, returning a trait object, or using a common concrete chain. The design decision is about representation and extensibility, not satisfying syntax.
Underscore and impl Trait communicate different promises
let values: _ = 0_u8..3; asks inference to fill a type hole in this local context. The inferred concrete type is fully usable and visible to type checking in the function.
-> impl Iterator<Item = u8> creates an abstraction boundary for callers. They know the declared capabilities but cannot rely on the concrete return type. The defining function must still return one compatible hidden type across its paths.
These are not interchangeable conveniences. One reduces local annotation work; the other controls an API surface.
Closures make the temptation stronger
Closure types cannot normally be written directly, so a local annotation like impl Fn(i32) -> i32 feels attractive. Inference handles a single closure. If several closure types must share storage, Box<dyn Fn(i32) -> i32> or a generic surrounding function can provide the required representation.
The same applies to long iterator adapter types. I let locals infer them and expose impl Iterator at a function boundary if callers should see only the iterator contract. Extracting a named helper can improve both readability and compile errors.
Opaque does not mean dynamically variable
Return-position impl Trait still represents one concrete hidden type per opaque definition. It cannot return a range in one branch and a filter adapter in another merely because both implement Iterator. This is where developers sometimes replace a local E0562 with a function and encounter E0308 next.
I write the concrete type of every branch on paper. If they differ, I decide whether to unify statically, use an enum, or erase dynamically. The compiler is forcing a real representation choice.
My diagnostic path is short
When E0562 appears, I classify the intent:
- If the initializer already determines one type, remove the annotation.
- If code needs one explicit concrete type, name it.
- If an API should hide one concrete return type, extract a function returning
impl Trait. - If several runtime types share one place, use a trait object or enum deliberately.
The core principle is that Rust's type abstraction forms have precise owners. Local inference lets the compiler name a concrete value for me. impl Trait in a signature defines who chooses a hidden concrete type. A trait object erases types at runtime. E0562 stops these three ideas from collapsing into an ambiguous placeholder syntax.