← all lessons
Types & Traits · #8 of 13

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

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:

One function, two types editable · real rustc
Open in Playground ↗ ready

Read the signature piece by piece:

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 bound is not satisfied editable · real rustc
Open in Playground ↗ ready

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:

A custom trait, two impls editable · real rustc
Open in Playground ↗ ready

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 */ }
Portrait of computer scientist Philip Wadler
Philip Wadler · b. 1956 Co-invented Haskell's type classes (1989), the direct ancestor of Rust's traits. He also popularised linear types in his paper 'Linear types can change the world!' — the idea underneath Rust's ownership system you met in lesson 4. Two of this language's pillars trace back to one person.

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:

One generic, four specialised copies editable · real rustc
Open in Playground ↗ ready

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:

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

1987

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.

1989

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.

1990

Wadler's 'Linear types can change the world!' argues that linear types make resource-handling safe — a second idea Rust would adopt, as ownership.

2010s

Rust unifies both threads: traits for shared behaviour, ownership for safe resources, both checked entirely at compile time.

Key takeaways

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.