Declaring a Trait and Writing the First `impl Trait for Type`
You have two structs, Article and Video, and each grows its own summary() function with the same signature and a different body. Every new type repeats the pattern, and the function that consumes summaries must know every concrete type in advance. A trait fixes that by naming the behavior once:
pub struct Article { pub title: String, pub body: String }
pub struct Video { pub title: String, pub seconds: u32 }
pub trait Summary {
fn summarize(&self) -> String;
}
A trait is a named contract: method signatures with no data and no storage behind them. impl Blocks and Methods attached behavior to a type you own with impl Type { ... }; sharing behavior across many types uses a second form, impl Trait for Type, and that is the only way to attach a trait's methods to a type:
impl Summary for Article {
fn summarize(&self) -> String {
format!("{} ({} chars)", self.title, self.body.len())
}
}
impl Summary for Video {
fn summarize(&self) -> String {
format!("{} ({}s)", self.title, self.seconds)
}
}
fn main() {
let a = Article { title: "Traits".into(), body: "long text".into() };
println!("{}", a.summarize()); // Traits (9 chars)
}
The compiler enforces three things on every impl. First, the signature must match the trait exactly: same receiver (&self here), same parameters, same return type — an impl that returns &'static str instead of String is rejected as an incompatible type for the trait, because callers were promised the trait's signature. Second, the method body is free: nothing about Article changes, no field is added, no base class is involved. Rust has no classes, so data lives in the struct, behavior lives in the trait, and impl ... for ... is the seam between them. Third — and this trips every newcomer — the trait must be in scope at the call site:
fn main() {
let a = Article { title: "x".into(), body: "y".into() };
println!("{}", a.summarize());
}
error[E0599]: no method named `summarize` found for struct `Article` in the current scope
|
= note: items from traits can only be used if the trait is implemented and in scope
Adding use crate::Summary; fixes it. Method resolution searches inherent methods first (those from your impl Article block), then every trait in scope; a trait that is implemented but never imported contributes nothing. That rule is why two traits with the same method name can coexist — and why you sometimes write Summary::summarize(&a) to disambiguate when both are in scope.
Note what this decoupling buys. The trait and the struct can live in different files, different crates, even different decades — you can implement your trait for your type, your trait for a foreign type such as String, or a foreign trait for your type. Only one of those four combinations is banned, and chapter three explains which.
A trait can also declare associated functions without a receiver (fn create() -> Self), called as Article::create() when unambiguous, and it can declare generics. For now the pattern you will use daily is a function that accepts anything implementing the trait:
fn report(item: &impl Summary) { println!("{}", item.summarize()); }
That signature is a promise about behavior, not a concrete type — the machinery that makes it precise is Generic Trait Bounds. Interviewers ask about traits constantly, and the question behind most of them is the same: how do you add behavior to a type you did not write, or to one whose implementation you need to swap in tests? The answer starts with impl Trait for Type.
Default Method Bodies and Supertraits: Sharing Behavior Without a Base Class
A trait where every method must be implemented by every type gets tedious fast, and it makes evolving a public trait dangerous. Default method bodies solve both:
pub trait Summary {
fn summarize(&self) -> String; // required: no body
fn preview(&self, width: usize) -> String { // defaulted: inherit it
let full = self.summarize(); // a default may call required methods
if full.chars().count() > width {
format!("{}…", full.chars().take(width).collect::<String>())
} else {
full
}
}
}
impl Summary for Video {
fn summarize(&self) -> String { format!("{} ({}s)", self.title, self.seconds) }
// preview() inherited as written — no code duplicated
}
Implementors choose per method: inherit the default, or override it. A default body may freely call the required methods, because every implementor is guaranteed to have them — that is the template-method shape: implement the one primitive (summarize), get a family of derived methods for free. Two limits are worth knowing. A default cannot invent a Self value — there is no Self { .. } literal and no Self::new() unless a supertrait guarantees a constructor — because the default code is shared by every implementor and does not know which one it is running for. And a default body is part of the trait's compiled interface: changing its logic changes behavior for every type that inherits it, at dependency-update time, with no edit in their code.
Why defaults matter most is backward compatibility. std::iter::Iterator is the proof: it has exactly one required method, next(), and dozens of defaults (for_each, copied, flatten, sum, enumerate) layered on over the years. Adding for_each in Rust 1.35 broke nobody, because no implementor had to write it. Add a required method to a public trait and every implementor in the ecosystem fails to compile — which is why library authors reach for a default body first, and why "can this new method be a default?" is a real design review question.
Supertraits are the other composition tool, and they run in the opposite direction:
pub trait Notify {
fn notify(&self);
}
pub trait Task: Notify { // every Task is also a Notify
fn run(&self);
}
impl Notify for Video {
fn notify(&self) { println!("done: {}", self.title); }
}
impl Task for Video {
fn run(&self) { println!("running {}", self.title); }
}
fn dispatch<T: Task>(item: &T) {
item.notify(); // legal: `T: Task` implies `T: Notify`
item.run();
}
Declaring Task: Notify means the compiler treats the bound T: Task as carrying T: Notify as well — you never write both at a call site. Skip the Notify impl and the trait declaration itself rejects the code:
error[E0277]: the trait bound `Video: Notify` is not satisfied
|
= help: the trait `Notify` is not implemented for `Video`
= note: required by a bound in `Task`
There is no base class to inherit from and no runtime check to pay for — the requirement is written in the trait header and proven at compile time.
Supertraits are also the reason a trait hierarchy must be considered before reaching for dyn: a supertrait that cannot become a trait object blocks the subtrait too, because the object's table must expose the supertrait's methods as well. That is the subject of Trait Objects and Dynamic Dispatch.
One more inheritance-flavored feature exists purely for tests and wrappers: a blanket impl<T: Summary> Summary for Wrapper<T> forwards to the inner value. It is legal because Summary is your trait — which brings up the rule everyone hits eventually.
The Orphan Rule: Why You Cannot Implement a Foreign Trait for a Foreign Type
You want Vec<u8> to print nicely, so you write impl std::fmt::Display for Vec<u8> in your crate. The compiler refuses:
error[E0117]: only traits defined in the current crate can be implemented for
types defined outside of the crate
|
4 | impl std::fmt::Display for Vec<u8> {
| ^^^^^-----------------------------
| impl doesn't use only types from inside the current crate
= note: upstream crates may add a new impl of trait `std::fmt::Display`
for type `std::vec::Vec<u8>` in future versions
This is the orphan rule, and it is coherence, not conservatism. The rule: in impl<...> Trait for Type, at least one of the trait or the type must be local to your crate, and generic parameters must be covered by a local type.
| Trait | Type | Allowed? | Why |
|---|---|---|---|
| local | local | yes | your crate, your rules |
| local | foreign (impl Summary for String) |
yes | you own the contract |
foreign (impl Display for User) |
local | yes | you own the type |
| foreign | foreign | no | E0117 |
Why the fourth row is fatal: suppose it were allowed. Your crate implements Display for Vec<u8>; next month the standard library adds its own; a third crate depends on both. Method resolution for write!(f, "{}", bytes) would have two candidate impls and no principled way to pick one — monomorphization would generate different code depending on dependency order. Rust refuses the ambiguity up front, and the note in the error is the giveaway: the real problem is that the upstream crate must remain free to add its own impl later without breaking yours.
The generic-parameter clause is the subtler half. impl<T> Display for MyVec<T> passes, because MyVec is local and covers T. But a bare type parameter never counts as local: impl<T> Display for T is rejected with E0210 (type parameter T must be covered by another type when they appear in the input types of a trait impl) — otherwise anyone's blanket impl would collide with everyone else's.
Two consequences show up in review. You cannot "specialize" a foreign trait for a subset of foreign types either — impl fmt::Display for Vec<T> where T: MyTrait is still foreign-plus-foreign, and a where clause does not make the type local. And note the direction of the fix: your traits stay open to the world, because implementing a local trait for any type — foreign or not — is always allowed. That asymmetry is why extensible Rust libraries publish traits rather than abstract base classes: downstream crates extend the interface themselves without asking you.
When the rule blocks you, the newtype is the standard escape, and it is exactly the wrapper introduced in Structs and Field Syntax:
use std::fmt;
pub struct ByteList(pub Vec<u8>); // local type, zero-size wrapper
impl fmt::Display for ByteList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for b in &self.0 { write!(f, "{b:02x}")?; }
Ok(())
}
}
ByteList is your type, so the impl is legal, and the cost is honest: construct with ByteList(bytes) and read with .0 (or add a Deref to smooth reads). The wrapper adds no memory and no branch — the distinction lives entirely in the type system. This pattern shows up everywhere in real crates: serializing a foreign type, adding a local trait bound to HashMap, or giving a foreign error type your crate's formatting. It is also why "implement a foreign trait for a foreign type" is an interview question with a layered answer: state the rule, then the coherence reason, then the newtype fix.
With traits declared, defaulted, and placed, the next step is making the compiler require them: Generic Trait Bounds.
Quiz
1. You call `article.summarize()` on a type that implements `Summary`, and the compiler answers with `error[E0599]: no method named 'summarize' found for struct 'Article' in the current scope`. What is wrong?
2. In your crate you write `impl std::fmt::Display for Vec<u8>`. What happens, and why?
3. How has `Iterator` gained dozens of methods (`for_each`, `sum`, `copied`, `flatten`) without breaking every implementor in the ecosystem?
4. Given `trait Task: Notify`, you write `impl Task for Video` but never `impl Notify for Video`. Which diagnostic do you get?
Flashcards
Question
What are the two forms for attaching behavior to a type, and what does each mean?
Click to reveal answer
Answer
`impl Type { ... }` adds inherent methods only that type has, and they win method resolution; `impl Trait for Type` attaches a shared contract that any compatible type may implement. A trait carries no data — fields stay in the struct — and because Rust has no classes, there is no inheritance layer between them.
Question
Why does calling a trait method without importing the trait produce E0599, and what are the fixes?
Click to reveal answer
Answer
Method resolution searches inherent impls first, then traits that are *in scope*; an unimported trait never enters the search. Fix with `use crate::Summary;` or the fully qualified `Summary::summarize(&a)`, which is also how you disambiguate when two in-scope traits declare the same method name.
Question
Why do default method bodies matter for a public trait's evolution?
Click to reveal answer
Answer
They make changes additive: a defaulted method compiles for every existing implementor, while a new required method breaks all of them (a semver-breaking change). `Iterator` is the proof — one required method (`next`) plus dozens of defaults layered on over the years.
Question
State the orphan rule and the escape when it blocks you.
Click to reveal answer
Answer
In `impl<...> Trait for Type`, at least one of trait or type must be local to your crate; bare type parameters never count as local (E0210). Foreign trait + foreign type = E0117. Fix: wrap the foreign type in a local newtype and implement for the wrapper — zero runtime cost, full type distinction.
Question
What does a supertrait guarantee, and what must hold for `dyn Sub` to work?
Click to reveal answer
Answer
`trait Task: Notify` means every `Task` implementor must also implement `Notify` (missing impl → E0277, `required by a bound in Task`), and a `T: Task` bound carries `Notify` without being written twice. Supertraits must be object safe as well, because `dyn Sub` has to expose their methods from the same vtable.
Revision Notes
Key Takeaways
- 1. A trait is data-free contract; `impl Trait for Type` is the only attachment mechanism — data stays in the struct, behavior in the trait, no inheritance layer between them
- 2. Trait methods must be in scope at the call site: missing `use` produces E0599, and inherent methods always resolve before trait methods
- 3. Default bodies make public traits evolvable — `Iterator` grew dozens of methods without breaking implementors, because only `next()` is required
- 4. Supertraits (`trait Task: Notify`) compose interfaces statically: a `T: Task` bound carries `Notify`, and both impls must exist
- 5. The orphan rule (E0117) bans foreign-trait-for-foreign-type impls to keep method resolution coherent; the newtype wrapper is the sanctioned escape
Interview Tips
- • "\"How do you add behavior to a type you don't own?\" — implement one of your traits for it, or wrap it in a newtype and implement the foreign trait for the wrapper; naming E0117 and the coherence reason shows you understand *why*, not just *what* fails"
- • "\"Why does E0599 say the trait must be in scope?\" — method resolution is inherent-methods-first, then imported traits; explain the fix (`use`) and the disambiguation form (`Summary::summarize(&a)`) when two traits collide"
- • "\"Why does Iterator only require `next()`?\" — every other method is a default built on it, which is how a public trait stays backward compatible; contrast with adding a required method, which breaks the ecosystem"
- • "\"What is the orphan rule?\" — give the table in two sentences (local trait OR local type), then the coherence argument about a downstream crate seeing two competing impls, then the newtype fix"
Cheat Sheet
Traits and Implementations
Declare and implement:
pub trait Summary {
fn summarize(&self) -> String; // required
fn preview(&self, w: usize) -> String { /* default, may call summarize() */ }
}
impl Summary for Article {
fn summarize(&self) -> String { format!("{}", self.title) }
}
Facts to keep straight:
| Fact | Detail |
|---|---|
| Attachment | impl Trait for Type — the only way; there are no classes or inheritance |
| Signature | receiver, params, and return type must match the trait exactly |
| Scope | trait must be used at the call site — otherwise E0599 with "items from traits can only be used if the trait is implemented and in scope" |
| Resolution | inherent methods win, then traits in scope; disambiguate with Trait::method(x) |
| Defaults | inheritable per method; may call required methods; cannot construct Self |
| Evolution | adding a required method breaks every implementor; a default body is additive (Iterator: next() + dozens of defaults) |
| Supertraits | trait Task: Notify ⇒ T: Task implies T: Notify; both impls required |
Orphan rule (coherence): at least one of trait/type must be local; generic params must be covered by a local type.
| Trait | Type | Legal? |
|---|---|---|
| local | foreign | ✅ your contract |
| foreign | local | ✅ your type |
| foreign | foreign | ❌ E0117 — escape with a newtype: struct Wrapper(Vec<u8>); |
Scope checklist: use the trait · match the signature · implement supertraits · keep impls coherent.