🎓 A1Learner
🔍

Rust impl Trait in Depth

impl Trait is shorthand for a hidden generic parameter. The shorthand can describe a value coming into a function or a value going out.

Two impl parameters, two types allowed

Each impl Display is a separate hidden parameter, so a number and a word can arrive together:

fn show_pair(a: impl Display, b: impl Display) {
    println!("pair: {a} and {b}");
}
show_pair(7, "seven");

When the two must match, name the letter

One T covering both parameters is a promise impl Trait cannot make. The two arguments must now be the same type. And same_type(3, "four") would be refused.

fn same_type<T: Display>(a: T, b: T) {
    println!("same: {a} and {b}");
}
same_type(3, 4);

Promise more with +

The caller of first_two knows two facts about the hidden type: it iterates over i32, and it can be cloned. Cloning the iterator before collecting lets both copies run:

fn first_two() -> impl Iterator<Item = i32> + Clone {
    vec![5, 6, 7, 8].into_iter().take(2)
}
let it = first_two();
let copy = it.clone();
println!("first run: {:?}", it.collect::<Vec<i32>>());
println!("clone run: {:?}", copy.collect::<Vec<i32>>());

impl Trait inside a trait

A trait may promise that a method returns some iterator, without naming its type. Each implementor chooses its own one concrete iterator type:

trait Playlist {
    fn titles(&self) -> impl Iterator<Item = String>;
}
struct Mix {
    songs: Vec<String>,
}
impl Playlist for Mix {
    fn titles(&self) -> impl Iterator<Item = String> {
        self.songs.clone().into_iter()
    }
}

Example

use std::fmt::Display;

fn show_pair(a: impl Display, b: impl Display) {
    println!("pair: {a} and {b}");
}

fn same_type<T: Display>(a: T, b: T) {
    println!("same: {a} and {b}");
}

fn first_two() -> impl Iterator<Item = i32> + Clone {
    vec![5, 6, 7, 8].into_iter().take(2)
}

trait Playlist {
    fn titles(&self) -> impl Iterator<Item = String>;
}

struct Mix {
    songs: Vec<String>,
}

impl Playlist for Mix {
    fn titles(&self) -> impl Iterator<Item = String> {
        self.songs.clone().into_iter()
    }
}

fn main() {
    show_pair(7, "seven");
    same_type(3, 4);

    let it = first_two();
    let copy = it.clone();
    println!("first run: {:?}", it.collect::<Vec<i32>>());
    println!("clone run: {:?}", copy.collect::<Vec<i32>>());

    let mix = Mix { songs: vec![String::from("intro"), String::from("finale")] };
    for title in mix.titles() {
        println!("track: {title}");
    }
}

Key points

  • Every impl Trait parameter is its own secret generic parameter.
  • Two of them in one signature may be filled by two different types
  • impl Trait cannot say "the same type as that other parameter".
  • When two positions must agree, name a generic instead.
  • Write fn f<T>(a: T, b: T)
  • Stack extra promises with +: -> impl Iterator<Item = i32> + Clone returns an iterator the caller may also clone
  • Since Rust 1.75, a trait method may return impl Trait too.
  • Each implementor still picks exactly one concrete return type
  • A method returning impl Trait cannot be called through a trait object.
  • dyn needs a return type it can name.
  • So such traits stay static-only (awareness)
  • It never mixes types.
  • The moment two different concrete types must flow through one place, that place needs dyn instead.

In this example, four impl Trait facts appear in one program. show_pair takes a number and a word in one call, because each impl Display is its own hidden parameter. same_type uses one named T instead, so both arguments must share a type — 3 and 4 do. first_two returns an iterator with two promises: Item = i32 and Clone. main clones it. Both copies collect [5, 6]. Last, the Playlist trait itself returns impl Iterator. Mix answers with a vector iterator over its cloned songs. The loop prints the two track names.

Note

impl Trait hides a type.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

In fn join(a: impl Display, b: impl Display), may the two arguments have different types?