RFA-424 · Case file with fixtures · Case 396 of 694 · Compiler evidence
Nested impl Trait in an Argument Needs a Named Generic Type
Argument-position impl Trait introduces an anonymous generic parameter, but nested anonymous parameters cannot be referenced coherently. Name the inner type and bind the outer argument to it when their relationship matters.
- 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
- Argument-position impl Trait introduces an anonymous generic parameter, but the inner type must be named when another generic relationship needs to refer to it.
- First discriminating check
- Name the inner type parameter and express the outer bound in terms of it, then decide whether a trait parameter or associated type models the relationship better.
I tried to make a function signature compact by writing impl Contains<impl Token>. Rust rejected it with E0666 because impl Trait cannot be nested inside the generic arguments of another impl Trait.
The failing fixture contains no difficult implementation body. The type expression itself is the problem.
Argument-position impl Trait is anonymous generics
In a function argument, this:
fn inspect(value: impl Display) {}
is close in purpose to:
fn inspect<T: Display>(value: T) {}
The caller chooses a concrete type implementing the bound. The impl Trait Reference describes argument-position use as an anonymous type parameter.
When I write impl Contains<impl Token>, I am trying to create an anonymous outer type and a second anonymous inner type while also relating them through Contains. Rust requires that inner parameter to be named.
Name the type whose identity matters
The repaired fixture writes:
fn accept<T: Token>(value: impl Contains<T>) {}
Now T names the inner token type, and the outer anonymous argument is constrained to implement Contains<T>. The relationship is explicit enough for bounds, additional arguments, return values, and diagnostics.
I can name both types when that reads better:
fn accept<T, C>(value: C)
where
T: Token,
C: Contains<T>,
{}
This form is longer but scales when several constraints refer to C or T.
Compact syntax stops helping when types relate
impl Trait is excellent when a parameter's concrete name is irrelevant to the rest of the signature. As soon as two positions must have the same type, an associated type must be projected, or one bound depends on another parameter, a name usually communicates the design better.
For example, two arguments written as impl Token may be two different concrete types. If they must match, I use one T: Token for both.
Anonymous does not mean dynamic. Argument-position impl Trait still uses static dispatch and monomorphisation in ordinary generic use. Adding more impl keywords does not create a runtime interface that can erase arbitrary nesting.
Return-position impl Trait has another owner
In return position, impl Trait hides one concrete type selected by the function body. The caller does not choose it. This difference matters when reading nested signatures.
Some associated type binding forms involving returned opaque types have their own language rules, but E0666's direct repair for the shown argument is a named generic parameter. I avoid generalising one allowed return form into every type position.
Trait objects solve a different problem
I could use dyn Token only if dynamic dispatch and object safety fit the design, and it must live behind a pointer or reference because its size is not known statically. Replacing generics with trait objects merely to avoid E0666 changes allocation, dispatch, lifetime, and supported-method behavior.
If the set of types is known and performance or associated types matter, named generics are normally the direct repair.
Associated types can model one inner type per implementor
Sometimes Contains<T> is not the best trait. If every container has exactly one token type, an associated type can express that:
trait Contains {
type Token: Token;
}
fn accept(value: impl Contains) {}
This removes the inner generic argument from the function. It also changes the trait's meaning: one implementation chooses one associated token type, rather than allowing the same container type to implement Contains for several token types.
I choose between a trait parameter and associated type based on that cardinality, not based only on signature length.
API evolution benefits from visible names
Named parameters make public documentation and compiler errors more precise. They also let me add bounds such as T: Send + 'static without a deeply nested expression.
The E0666 page gives the named-generic repair directly. I treat the diagnostic as a design prompt: which type relationship was important enough that anonymity stopped working?
My checklist
- Does the inner concrete type need to appear elsewhere?
- Must two inputs share one concrete type?
- Can the outer trait support several inner types or exactly one?
- Would an associated type express that choice better?
- Is dynamic dispatch actually required?
- Will named generics improve public diagnostics?
The core principle is that type relationships need handles. Argument-position impl Trait hides a name when no relationship needs it. Once an opaque parameter is nested as another trait's type argument, I name the inner type and make the constraint readable instead of stacking anonymity.