Coding by Hand
Rust home

Traits

A trait in Rust is a contract sitting on a desk. Anyone who walks up and signs it joins a club, and the dues are simple — you promise to provide every method the contract lists. The contract does not care what you are. A dog can sign. A robot can sign. A coffee cup can sign. Once your signature is on the page, every part of the program that asks for a club member knows it can call the contracted methods on you and get something sensible back. That single trick — say what behavior you need, not what type you want — is how a strict language like Rust still lets unrelated types share code.

A trait is a printed contract on a desk that any type can walk up and sign.
A trait is a printed contract on a desk that any type can walk up and sign.

The idea is older than Rust by 30 years. Barbara Liskov at MIT built a language called CLU in 1975 and called the contract an "abstract type" — code could ask for any value that supplied a fixed set of operations and the compiler would check it. The Haskell team in the late 1980s pulled the same idea into a feature they called type classes, and Phil Wadler proved you could compile it without runtime overhead by stamping out a fresh copy of the function for every concrete type the caller used. Graydon Hoare at Mozilla wanted that exact property for Rust — shared behavior with no virtual table tax — and renamed type classes to "traits." The word came from a 2003 Smalltalk paper on small, focused bundles of methods. The name stuck because it described what the thing actually does — it carves out one slice of behavior and lets a type wear it.

Here is the contract. Two methods, but one is already filled in.

trait Greet {
    fn greet(&self) -> String;

    fn shout(&self) -> String {
        self.greet().to_uppercase()
    }
}

The line fn greet(&self) -> String; ends in a semicolon, which is the contract's way of saying every signer must provide their own. The next method, shout, comes with a body — that is a default. If a signer wants their own version they can override it, but if they say nothing they get the default for free. Defaults are the lever that lets a trait carry useful shared logic without forcing every type to copy and paste. The default knows the signer provides greet, so it leans on greet and builds the louder version on top.

Now two types ready to sign. A Dog with a name. A Robot with an id. Notice they have nothing in common — different fields, different shapes, no inheritance.

struct Dog {
    name: String,
}

struct Robot {
    id: u32,
}

The signature itself is an impl block per type. Each one fills in greet in the way that fits the type, and neither one bothers with shout because the default already handles it.

impl Greet for Dog {
    fn greet(&self) -> String {
        format!("woof, I'm {}", self.name)
    }
}

impl Greet for Robot {
    fn greet(&self) -> String {
        format!("beep, unit {} reporting", self.id)
    }
}

There is a rule sitting underneath that block that the compiler quietly enforces, and it has a strange name — the orphan rule, or coherence rule. You can write impl Greet for Dog here only because you defined either the trait or the type in this same crate. If Greet lived in someone else's library and Dog lived in a third library, you would not be allowed to glue them together in your own code. Rust would refuse. The reason is that two different crates could each write their own conflicting impl for the same pair, and the compiler would have no way to pick one. Coherence guarantees that for any trait and any type, there is exactly one implementation in the whole program, and it lives in the same crate as one of them. You feel the rule the first time you try to add a method to a Vec from outside the standard library — the compiler stops you and you write a wrapper struct instead.

main builds one of each type and calls both methods on both. Same contract, two signers, two answers.

fn main() {
    let rex = Dog {
        name: String::from("Rex"),
    };
    let unit = Robot { id: 7 };

    println!("each type signs the same Greet contract:");
    println!("  Dog   .greet() -> {}", rex.greet());
    println!("  Robot .greet() -> {}", unit.greet());
    println!();

    println!("the default shout() works for both:");
    println!("  Dog   .shout() -> {}", rex.shout());
    println!("  Robot .shout() -> {}", unit.shout());
}

Run it and watch the contract pay out.

each type signs the same Greet contract:
  Dog   .greet() -> woof, I'm Rex
  Robot .greet() -> beep, unit 7 reporting

the default shout() works for both:
  Dog   .shout() -> WOOF, I'M REX
  Robot .shout() -> BEEP, UNIT 7 REPORTING

Read the top three lines and the bottom three. The dog and the robot answered greet with completely different sentences, because each one wrote its own. The dog and the robot both answered shout correctly and neither type wrote a single line of shout code — that came from the trait's default, calling whichever greet belonged to the actual type at the call site. The contract knew enough about the shape of the club to deliver useful behavior without ever knowing what kind of member it was talking to.

Two unrelated types signing the same Greet contract.
Two unrelated types signing the same Greet contract.

One last thing the compiler is doing behind the curtain. When you write rex.greet(), Rust decides at compile time exactly which function to call — the one in impl Greet for Dog. It stamps a direct call into the machine code with no lookup at runtime. That is called static dispatch, and it is why Haskell's old trick of "type classes compile to nothing" carried into Rust. The cost of using a trait is the cost of calling a regular function. There is a second mode, where the program holds a bag of mixed signers and decides who to call from a little table at runtime, written dyn Greet. That mode trades one pointer lookup for the ability to mix types in the same collection, and the next lesson on trait objects is where that bag gets unpacked.