16.4impl Trait
Trait bounds like <T: Summary> are precise but sometimes heavier than a simple case needs. Rust offers a lighter syntax, impl Trait, that reads as "some type that implements this trait." It works in two places, function arguments and return types, and each solves a real ergonomic problem. It's not a separate feature so much as a convenient spelling of what you already learned.
impl Trait in argument position
When a function takes a generic parameter it uses only through a trait, and doesn't need to name the type, you can replace <T: Trait> with impl Trait right in the parameter list:
trait Summary {
fn summarize(&self) -> String;
}
fn notify(item: &impl Summary) {
println!("Breaking news: {}", item.summarize());
}
item: &impl Summary means "a reference to some type that implements Summary." This is exactly equivalent to the generic form:
fn notify<T: Summary>(item: &T) {
println!("Breaking news: {}", item.summarize());
}
The impl Trait version is shorter and reads more directly for the common case of "I just need something summarizable." Use it when you have one parameter, use it through its trait, and don't need to refer to its type by name elsewhere in the signature. Reach for the explicit <T: Trait> form when you need to name T (say, two parameters that must be the same type, which fn pair<T: Summary>(a: &T, b: &T) expresses but two separate impl Summarys do not).
impl Trait in return position
The more powerful use is as a return type, where it solves a problem the explicit generic form can't. Often a function returns a value whose type is real but tedious or impossible to write out, especially with closures and iterators (chapter 19). impl Trait lets you promise the capability without naming the type:
fn make_greeter() -> impl Fn(&str) -> String {
|name| format!("Hello, {name}!")
}
fn main() {
let greet = make_greeter();
println!("{}", greet("Ada"));
}Hello, Ada!
make_greeter returns impl Fn(&str) -> String, read "some type that is a function from &str to String." The actual returned thing is a closure (chapter 19), whose concrete type has no name you could write. impl Trait in return position says "I'm returning something with this behavior; you don't need to know its exact type, only what it can do." This is the standard way to return closures and iterators, and you'll see it everywhere once chapter 19 makes those central.
Key insight
impl Trait in return position lets a function hide its concrete return type behind a capability. The caller learns "you get something that's a Fn" or "something that's an Iterator", which is all they need, while the function keeps the freedom to return whatever specific type is convenient. It's the return-value counterpart to taking &impl Summary as a parameter: name the behavior, not the type.
A limitation worth knowing
impl Trait in return position has one catch: a function can return only one concrete type, even though that type is unnamed. This compiles fine (every path returns the same closure type), but a function that tried to return either a closure or a different impl Fn from different branches would be rejected, because impl Trait is still one hidden concrete type chosen by monomorphization (lesson 15.5), not a runtime choice between types.
fn make_op(add: bool, by: i32) -> impl Fn(i32) -> i32 {
if add {
move |x| x + by
} else {
move |x| x - by // refused: a different closure type than the other arm
}
}error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:3:9
|
2 | if add {
| _____________-
3 | | move |x| x + by
| | --------------- expected because of this
4 | | } else {
5 | | move |x| x - by
| | ^^^^^^^^^^^^^^^ expected closure, found a different closure
6 | | }
| |_____- `if` and `else` have incompatible types
|
= note: no two closures, even if identical, have the same type
= help: consider boxing your closure and/or using it as a trait object
Each closure captures by, so they're genuine distinct closure types (non-capturing closures would quietly coerce to a shared function-pointer type and slip through). The two closures have different concrete types, so they can't both be the single hidden type impl Fn stands for. When you genuinely need to return one of several different types decided at runtime, that's the job of a trait object (Box<dyn Trait>, lesson 16.9), which the error here will eventually point you toward. For the common case of returning one consistent unnamed type, impl Trait is exactly right.
Best practice
Use impl Trait in argument position for simple "takes something with this behavior" parameters, and the explicit <T: Trait> form when you need to name the type or constrain several parameters to the same one. Use impl Trait in return position to return closures and iterators whose types are unwieldy or unnameable. When you need to return genuinely different types from different branches, switch to Box<dyn Trait> (lesson 16.9).
Quiz time
Question #1
Rewrite fn notify<T: Summary>(item: &T) using impl Trait, and say when you'd prefer the original generic form.
Show solution
fn notify(item: &impl Summary). The two are equivalent. Prefer the explicit <T: Summary> form when you need to name the type, for instance to require two parameters to be the same type (fn f<T: Summary>(a: &T, b: &T)), which two separate &impl Summary parameters wouldn't enforce.
Question #2
Why is impl Trait especially useful as a return type?
Show solution
Because some return values (closures, iterators) have concrete types that are tedious or impossible to write by name. impl Trait in return position lets the function promise the behavior ("returns something that's a Fn" or "an Iterator") without naming the type, which is the standard way to return closures and iterators.
Question #3
Why is returning impl Fn(...) from two different if/else branches with two different closures refused?
Show solution
Because impl Trait in return position is still a single hidden concrete type (chosen at compile time, lesson 15.5), and two different closures have two different concrete types. The function must return one consistent type. To return genuinely different types decided at runtime, use a trait object like Box<dyn Fn(...)> (lesson 16.9).
You've implemented traits by hand and bounded generics with them. But for common traits like Debug and Clone, writing the implementation by hand would be pure boilerplate. The next lesson is the tour of derive: which traits you can ask the compiler to write for you, and what each one unlocks.