Traits and Generics — Code That Works for Many Types
Polymorphism without inheritance, and the static-vs-dynamic dispatch trade-off
You already know how to write a function that sums a list of integers. But what if you wanted one function that finds the largest item in any list — numbers, words, dates — without writing it three times? And what if the compiler could promise that the result runs exactly as fast as the hand-written version?
That promise has a name, monomorphization, and the contract that makes it safe has a name too: the trait.
You’ve written functions for specific types: sum(xs: &[i32]) sums i32s. What if you want a print_summary function that works for any type, as long as that type knows how to summarise itself? That’s the question this lesson answers. The mechanism is traits (Rust’s name for a contract a type can satisfy) plus generics (placeholders for “any type”). Together they let you write code once and have it work for many concrete types — without inheritance, the way C++ and Java do this.
This is the most jargon-heavy lesson so far. We define every term as we hit it.
A few words you’ll need
- Polymorphism: a Greek-rooted word that means “one piece of code can work with many different types.” You’ve been using polymorphism without naming it —
println!polymorphically formats integers, strings, booleans, your own types. The interesting question is how a language provides polymorphism. Rust’s answer is traits + generics, which we’ll see in a minute. - Trait: a named contract — a list of methods a type must provide. Once a type implements a trait, the type can be used anywhere that trait is required.
- Generic: a placeholder for “some type, decided by the caller.” Written with angle brackets:
Vec<T>is “a Vec of some typeT”; the caller picks theT. - Bound: a restriction on what types fit a generic placeholder.
T: Displaymeans “any type T, but only if T implements the Display trait.” Bounds are how you say “I need this generic type to have certain capabilities.” - Monomorphization: the compiler’s trick of stamping out one specialised copy of a generic function per concrete type used. It’s why generics in Rust cost nothing at runtime.
A generic function with a bound
Here’s the classic example. We want the largest item in a slice, no matter what the items are. The only thing we need from them is the ability to be compared, which is exactly what the PartialOrd trait promises.
Hit Run. The same function handles a list of numbers and a list of words:
Read the signature piece by piece:
fn largest— the function’s name.<T: PartialOrd>— introduces a generic placeholder namedT, with a bound:Tmust implement thePartialOrdtrait (soitem > biggestis even meaningful).(list: &[T])— takes a slice ofTs (slices were lesson 6).-> &T— returns a reference to one of them.
The bound is not red tape; it’s what makes the > inside the body legal. Without T: PartialOrd, the compiler would have no idea whether two Ts can be compared, and would refuse to build.
When a type doesn’t satisfy the bound
Here is the whole point of a bound: it’s checked at compile time, before your program ever runs. Below we feed largest a list of Points — a struct we never taught how to compare. Hit Run and read the error:
The compiler reports error[E0277]: the trait PartialOrd is not implemented for Point. It even points at the exact bound that was violated and suggests a fix — #[derive(PartialOrd)] on the struct. This is the contract failing loudly and early. In a language with weaker types, a “compare two points” bug like this slips through to runtime, or silently does something nonsensical. Rust catches it at the door.
Defining and implementing your own trait
PartialOrd is built in, but the real power is writing your own contracts. A trait declares a list of methods a type must provide:
trait Summary {
fn summary(&self) -> String;
}
Any type can then implement it. The implementation says what summary actually does for that specific type. Run this — one print_summary function works for two completely unrelated structs:
print_summary is written once and works for both. Add a new type later — Email, BlogPost, anything — that implements Summary, and it instantly becomes compatible with print_summary. No edits needed. The &impl Summary parameter is shorthand for “a reference to some type that implements Summary”; it’s exactly equivalent to writing <T: Summary>(item: &T).
Rust offers three interchangeable ways to spell a bound. They compile to identical code — pick whichever reads cleanest:
fn print_summary<T: Summary>(item: &T) { /* classic */ }
fn print_summary(item: &impl Summary) { /* impl Trait — cleanest for one bound */ }
fn print_summary<T>(item: &T) where T: Summary { /* where clause — best when bounds get long */ }
Generics are zero-cost: monomorphization
Here’s the part that makes Rust special. When you call largest(&numbers) and largest(&words), you might imagine the program carrying around some runtime machinery to handle “any type T.” It does not. At compile time, Rust performs monomorphization: it looks at every concrete type a generic was actually called with, and stamps out a separate, specialised copy of the function for each one.
So largest<i32> and largest<&str> become two distinct functions in the final binary — each one hard-coded for its exact type, each one as fast as if you’d written it by hand. The word “monomorphization” is intimidating; the idea is simple: turn one generic function into many concrete functions.
Run this and picture four specialised copies of announce being generated, one per type:
The T: Display bound is what lets the body write {value} — Display is the trait that means “this type knows how to print itself as human-readable text.”
The other strategy: dynamic dispatch with dyn
Monomorphization decides which summary to call at compile time — that’s called static dispatch, and it’s the default. There’s a second strategy, dynamic dispatch, where the program decides while running by looking the method up in a small table called a vtable (one extra memory hop per call). You ask for it with the keyword dyn:
fn print_dyn(item: &dyn Summary) {
println!("{}", item.summary());
}
You need dyn when you want a single collection holding values of different concrete types that share a trait:
let feed: Vec<Box<dyn Summary>> = vec![
Box::new(Article { /* ... */ }),
Box::new(Tweet { /* ... */ }),
];
for item in &feed {
println!("{}", item.summary());
}
A Vec<Article> could only hold articles. A Vec<Box<dyn Summary>> holds anything that implements Summary, each accessed through its vtable. The rule of thumb:
- Default to static dispatch (
<T: Trait>orimpl Trait). Faster, and the binary-size cost rarely matters. - Reach for
dyn Traitwhen you genuinely need a heterogeneous collection, or you’re designing a public API and don’t want to commit to one concrete type.
Blanket impls — and why Rust has no inheritance
You can implement a trait for every type that satisfies some bound. This is how the standard library gives every Display-able thing a to_string() method, in one stroke:
// simplified from the real stdlib
impl<T: Display> ToString for T {
fn to_string(&self) -> String { /* uses Display under the hood */ }
}Read the head: “for any type T that implements Display, here is a ToString impl.” This is a blanket impl. The payoff: implement Display for your own type, and you get .to_string() for free. Whole capability stacks compose this way.
This is also the answer to “why does Rust have no inheritance?” Other languages let one type be a subtype of another (a Dog is an Animal, inheriting its data and methods). Rust instead says: Animal is a contract; Dog satisfies it by providing its own implementation of every method, and so does Cat, and so does Snake. A type can satisfy many contracts without being a “kind of” anything. You lose automatic sharing of data between related types; you gain enormous flexibility in how you compose behaviour. For almost every Rust program, that trade is a win.
A short history of the idea
Jean-Yves Girard introduces linear logic — the theory that values can be used a fixed number of times. It will later underpin Rust's ownership.
Philip Wadler and Stephen Blott publish type classes for Haskell — the principled way to give one operation to many types. Rust's traits are their direct descendant.
Wadler's 'Linear types can change the world!' argues that linear types make resource-handling safe — a second idea Rust would adopt, as ownership.
Rust unifies both threads: traits for shared behaviour, ownership for safe resources, both checked entirely at compile time.
Key takeaways
- A trait is a contract: a list of methods a type promises to provide. Implement one with
impl Trait for Type { ... }. - A generic (
<T>) is a placeholder for “any type, picked by the caller.” Add a bound (<T: Trait>) to require capabilities — and the bound is checked at compile time (error[E0277]when it fails). - Three equivalent ways to express a bound:
<T: Trait>,impl Trait,where T: Trait. Use whichever reads best. - Monomorphization stamps out a specialised copy of a generic per concrete type, so generics are zero-cost at runtime — unlike Java’s erased, boxed generics.
- Static dispatch (the default) decides the call at compile time; dynamic dispatch (
dyn Trait) decides it at runtime via a vtable — needed for heterogeneous collections. - Rust has no inheritance. Composition through traits is more flexible and usually nicer to refactor.
Traits and ownership — the two halves of Rust’s soul — both trace back to the same restless corner of computer science, and even to the same person. One gives you safe behaviour shared across types; the other gives you safe resources. Both are checked before your program runs, and both cost nothing once it does.
Next, lesson 9 turns these contracts into pipelines: closures and iterators, where traits like Iterator let you describe a whole data transformation as a single, lazy, zero-cost expression.