Search code examples
genericsrusttrait-objects

Implement Trait for generic caller?


I was just reading this article about generics and Traits in the Rust by Example, where they seem to implement a Trait on a generic caller. Check their article for the full example, but here's a minimal working example:

struct Example;

trait Foo {
  fn print_foo(&self);
}

impl<T> Foo for T {
  fn print_foo(&self) {
    println!("foo");
  }
}

fn main() {
  let example = Example;
  example.print_foo();
}

This example works and prints foo. How does this work? For context, I read the full Rust book and didn't see this mentioned until the aforementioned article. How exactly does a generic implementation like this work? How does the compiler know to associate print_foo with Example? How is this impl scoped? Could I have crate-scoped generic implementations?


Solution

  • How exactly does a generic implementation like this work?

    You can more or less think about generics as if they were lazy code generation: impl<T> Foo for T means “For every¹ concrete type T, if an implementation of Foo for T is needed, create one using this function.”

    How does the compiler know to associate print_foo with Example?

    When the compiler sees a method call, here .print_foo(),

    • It first looks for a matching inherent impl, impl Foo { fn print_foo(&self) {...} }. It doesn't find one in this case.
    • It looks at all traits that are visible in the current scope (defined or used) to see if any of them define a method called print_foo.
    • It checks whether Example implements any of these traits. Example implements Foo, because everything (except unsized types) implements Foo, so that trait is used.

    These rules are described in the Method-call expressions section of the Rust Reference.

    How is this impl scoped? Could I have crate-scoped generic implementations?

    Trait implementations are not scoped. They effectively exist everywhere, regardless of what might be brought into the current scope. (The trait implementation coherence rules are designed to ensure that which implementations are found never depends on which crates are in the current crate's dependencies, i.e. which code the compiler compiled.)

    However, traits are scoped: a trait method will never be found unless the trait is brought into scope with use or by defining it in the same module.

    In your particular case, impl<T> Foo for T means that it is not possible to write any other implementations for the trait¹ — there is no such thing as a “crate-scoped generic implementation”.


    ¹ <T> does not quite mean “every type”. It means “every sized type”, because most generic code can't handle dynamically-sized/“unsized” types like dyn Foo and [i32], so it's a useful default restriction. If you write <T: ?Sized> instead, then the generic is truly over every possible type.