Post

Traits: Behavioral Contracts the Compiler Enforces

Traits define shared behavior across types — and the compiler guarantees every type that claims to implement a trait actually does. This is polymorphism with compile-time proof, not runtime hope.

Traits: Behavioral Contracts the Compiler Enforces

In most languages, “shared behavior” is a gentleman’s agreement. You define an interface, a class says it implements it, and if it doesn’t — well, you find out at runtime. Maybe. Rust doesn’t do gentleman’s agreements. It does traits.

A trait defines a set of methods that a type must implement. If a type says it implements a trait, the compiler checks that every required method exists with the correct signature. If it doesn’t, the program doesn’t compile. The behavioral contract is enforced before your code ever runs.

Defining a Trait

A trait is a collection of method signatures (and optionally, default implementations) that describe a behavior:

1
2
3
trait Summarizable {
    fn summary(&self) -> String;
}

This says: any type that is Summarizable must provide a summary method that takes an immutable reference to itself and returns a String. That’s the contract. Nothing more, nothing less.

Implementing a Trait

Let’s create two types that implement Summarizable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct BlogPost {
    title: String,
    author: String,
    content: String,
}

struct Tweet {
    username: String,
    body: String,
    reply: bool,
}

impl Summarizable for BlogPost {
    fn summary(&self) -> String {
        format!("{} by {} — {}", self.title, self.author, &self.content[..50])
    }
}

impl Summarizable for Tweet {
    fn summary(&self) -> String {
        format!("@{}: {}", self.username, self.body)
    }
}

Two completely different types, each with their own data layout, but both guarantee the same behavior: call .summary() and you get a String back. The compiler verified both implementations match the trait signature.

Now try calling .summary() on a type that doesn’t implement Summarizable:

1
2
3
4
5
6
7
8
struct RandomStruct {
    value: i32,
}

fn main() {
    let r = RandomStruct { value: 42 };
    println!("{}", r.summary());
}
1
2
3
4
5
6
7
8
9
10
error[E0599]: no method named `summary` found for struct `RandomStruct` in the current scope
 --> src/main.rs:8:24
  |
1 | struct RandomStruct {
  | ------------------- method `summary` not found for this struct
...
8 |     println!("{}", r.summary());
  |                      ^^^^^^^ method not found in `RandomStruct`
  |
  = help: items from traits can only be used if the trait is implemented and in scope

The compiler doesn’t guess. It doesn’t try to find a method with a similar name on some parent class. If the type hasn’t implemented the trait, the method doesn’t exist. Period.

Default Implementations

Sometimes a trait can provide a reasonable default:

1
2
3
4
5
trait Summarizable {
    fn summary(&self) -> String {
        String::from("(Read more...)")
    }
}

Types that implement Summarizable can now choose: override summary with their own logic, or use the default. Either way, the contract holds — calling .summary() always returns a String.

1
2
3
4
5
6
7
8
9
10
struct QuickNote {
    text: String,
}

impl Summarizable for QuickNote {}

fn main() {
    let note = QuickNote { text: String::from("remember to buy milk") };
    println!("{}", note.summary()); // prints: (Read more...)
}

QuickNote gets the default behavior for free. It still satisfies the trait contract without writing any method body.

Traits as Parameters

Here’s where traits become powerful. You can write functions that accept any type that implements a specific trait:

1
2
3
fn print_summary(item: &impl Summarizable) {
    println!("{}", item.summary());
}

This function doesn’t care whether you pass a BlogPost, a Tweet, or a QuickNote. It only cares that the type implements Summarizable. The compiler checks this at every call site — pass something that doesn’t implement the trait, and it won’t compile.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
    let post = BlogPost {
        title: String::from("Traits in Rust"),
        author: String::from("Shreyas"),
        content: String::from("Traits define shared behavior across types and the compiler..."),
    };

    let tweet = Tweet {
        username: String::from("rustlang"),
        body: String::from("Traits are great!"),
        reply: false,
    };

    print_summary(&post);
    print_summary(&tweet);
}

Both calls compile because both types satisfy the contract.

Trait Bounds

The impl Trait syntax is shorthand. The full syntax uses trait bounds:

1
2
3
fn print_summary<T: Summarizable>(item: &T) {
    println!("{}", item.summary());
}

This says: T can be any type, as long as it implements Summarizable. Same guarantee, more explicit. When you need multiple bounds, the syntax scales:

1
2
3
4
fn display_and_summarize<T: Summarizable + std::fmt::Display>(item: &T) {
    println!("Display: {}", item);
    println!("Summary: {}", item.summary());
}

Now T must implement both traits. The compiler checks both contracts. You can also use the where clause when bounds get long:

1
2
3
4
5
6
fn do_stuff<T>(item: &T)
where
    T: Summarizable + std::fmt::Display + Clone,
{
    // ...
}

Every bound you add is a constraint the compiler enforces. More bounds means a stricter contract — the type must satisfy all of them.

Common Standard Library Traits

Rust’s standard library is built on traits. Here are the ones you’ll encounter constantly:

Display — defines how a type is formatted with {}:

1
2
3
4
5
6
7
8
9
10
11
12
use std::fmt;

struct Player {
    name: String,
    score: u32,
}

impl fmt::Display for Player {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} (score: {})", self.name, self.score)
    }
}

Debug — defines how a type is formatted with {:?}. Usually derived:

1
2
3
4
5
#[derive(Debug)]
struct Player {
    name: String,
    score: u32,
}

The #[derive(Debug)] attribute auto-generates the implementation. The invariant: any type with Debug can be printed in debug format. No runtime surprises.

Clone — explicit duplication of a value:

1
2
3
4
5
6
7
8
9
10
11
#[derive(Clone)]
struct Player {
    name: String,
    score: u32,
}

fn main() {
    let p1 = Player { name: String::from("Alice"), score: 100 };
    let p2 = p1.clone(); // deep copy
    println!("{} and {}", p1.name, p2.name); // both valid
}

Copy — implicit duplication for simple types. Copy requires Clone and can only be derived for types that live entirely on the stack:

1
2
3
4
5
6
7
8
9
10
11
#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 10, y: 20 };
    let p2 = p1; // copies, doesn't move
    println!("{:?} {:?}", p1, p2); // both valid
}

Try deriving Copy on a type that contains a String:

1
2
3
4
5
6
7
8
error[E0204]: the trait `Copy` may not be implemented for this type
 --> src/main.rs:1:24
  |
1 | #[derive(Debug, Clone, Copy)]
  |                        ^^^^
2 | struct Player {
3 |     name: String,
  |     ------------ this field does not implement `Copy`

The compiler enforces the contract: Copy is only for types where bitwise duplication is safe. A String owns heap memory — copying the bits would create two owners, violating ownership. The trait bound prevents it.

The Contract Pattern

Every trait in Rust follows the same pattern:

TraitContract
DisplayThis type can be formatted for user-facing output
DebugThis type can be formatted for debugging
CloneThis type can be explicitly duplicated
CopyThis type can be implicitly duplicated (stack-only)
PartialEqThis type can be compared for equality
IteratorThis type produces a sequence of values

Each row is a behavioral guarantee. If a type implements the trait, it upholds the contract. The compiler verifies the implementation matches the trait definition. No runtime checks. No duck typing. No “method not found” at 3 AM in production.

This is what distinguishes Rust’s polymorphism from dynamic dispatch in languages like Python or JavaScript. When you write a function that accepts impl Display, you know — at compile time, with certainty — that every type passed to it can be displayed. The invariant is structural, not hopeful.


Traits turn behavioral expectations into compiler-checked contracts. You define the behavior, types opt in, and the compiler verifies the implementation. Next up: generics — writing code that works across many types while the compiler still checks every single one.

This post is licensed under CC BY 4.0 by the author.