Traits and impl blocks
What is a trait?
In the previous chapter, you saw how enums let a single type have multiple variants. Traits solve the opposite problem: letting multiple types share a common interface.
A trait is a contract. It says "any type that implements this trait must provide these functions." Think of it like a job description — it lists what the job requires, but different people (types) fulfill those requirements differently.
Your first trait
trait Describable { fn describe(self) -> String }
This trait says: "any type that is Describable must have a describe method that takes itself and returns a String."
The self parameter is special — it refers to the value the method is called on.
Implementing a trait
Use impl TraitName for TypeName to fulfill the contract:
trait Describable { fn describe(self) -> String } struct Dog { name: String, breed: String } struct Car { make: String, year: int } impl Describable for Dog { fn describe(self) -> String { return self.name + " the " + self.breed } } impl Describable for Car { fn describe(self) -> String { return int_to_string(self.year) + " " + self.make } } fn main() { let d: Dog = Dog { name: "Rex", breed: "German Shepherd" } let c: Car = Car { make: "Toyota", year: 2024 } print(d.describe()) // Rex the German Shepherd print(c.describe()) // 2024 Toyota }
Both Dog and Car implement Describable, but each provides its own version of describe. The method is called with dot syntax: d.describe().
Methods without traits: impl blocks
You do not always need a trait to add methods to a type. A plain impl block adds methods directly:
struct Rectangle { width: int, height: int } impl Rectangle { fn area(self) -> int { return self.width * self.height } fn perimeter(self) -> int { return 2 * (self.width + self.height) } fn is_square(self) -> bool { return self.width == self.height } fn scale(self, factor: int) -> Rectangle { return Rectangle { width: self.width * factor, height: self.height * factor } } } fn main() { let r: Rectangle = Rectangle { width: 10, height: 5 } print(r.area()) // 50 print(r.perimeter()) // 30 print(r.is_square()) // false let big: Rectangle = r.scale(3) print(big.area()) // 450 }
Methods in impl blocks are called with dot syntax, just like trait methods. The first parameter is always self.
Multiple traits on one type
A type can implement as many traits as needed:
trait Display { fn to_string(self) -> String } trait Eq { fn equals(self, other: Self) -> bool } struct Point { x: int, y: int } impl Display for Point { fn to_string(self) -> String { return "(" + int_to_string(self.x) + ", " + int_to_string(self.y) + ")" } } impl Eq for Point { fn equals(self, other: Point) -> bool { return self.x == other.x and self.y == other.y } } impl Point { fn distance_squared(self, other: Point) -> int { let dx: int = self.x - other.x let dy: int = self.y - other.y return dx * dx + dy * dy } } fn main() { let a: Point = Point { x: 3, y: 4 } let b: Point = Point { x: 3, y: 4 } let c: Point = Point { x: 1, y: 1 } print(a.to_string()) // (3, 4) print(a.equals(b)) // true print(a.equals(c)) // false print(a.distance_squared(c)) // 13 }
Traits as function parameters
You can write functions that accept any type implementing a trait:
trait Display { fn to_string(self) -> String } struct Person { name: String, age: int } struct Product { name: String, price: int } impl Display for Person { fn to_string(self) -> String { return self.name + " (age " + int_to_string(self.age) + ")" } } impl Display for Product { fn to_string(self) -> String { return self.name + " - $" + int_to_string(self.price) } } fn print_item<T: Display>(item: T) { print("Item: " + item.to_string()) } fn main() { let p: Person = Person { name: "Alice", age: 30 } let prod: Product = Product { name: "Laptop", price: 999 } print_item(p) // Item: Alice (age 30) print_item(prod) // Item: Laptop - $999 }
The syntax means "T can be any type, as long as it implements Display." This is called a trait bound.
Practical example: a shape system
trait Shape { fn area(self) -> int fn name(self) -> String } struct Circle { radius: int } struct Square { side: int } struct Triangle { base: int, height: int } impl Shape for Circle { fn area(self) -> int { return 3 * self.radius * self.radius } fn name(self) -> String { return "Circle" } } impl Shape for Square { fn area(self) -> int { return self.side * self.side } fn name(self) -> String { return "Square" } } impl Shape for Triangle { fn area(self) -> int { return self.base * self.height / 2 } fn name(self) -> String { return "Triangle" } } fn print_shape<T: Shape>(s: T) { print(s.name() + " has area " + int_to_string(s.area())) } fn main() { let c: Circle = Circle { radius: 10 } let sq: Square = Square { side: 7 } let t: Triangle = Triangle { base: 6, height: 8 } print_shape(c) // Circle has area 300 print_shape(sq) // Square has area 49 print_shape(t) // Triangle has area 24 }
Practical example: a scoring system
trait Scoreable { fn score(self) -> int fn label(self) -> String } struct Student { name: String, average: int } struct Team { name: String, wins: int, losses: int } impl Scoreable for Student { fn score(self) -> int { return self.average } fn label(self) -> String { return "Student " + self.name } } impl Scoreable for Team { fn score(self) -> int { return self.wins * 3 } fn label(self) -> String { return "Team " + self.name } } fn print_ranking<T: Scoreable>(item: T) { print(item.label() + ": " + int_to_string(item.score()) + " points") } fn main() { let s: Student = Student { name: "Alice", average: 95 } let t: Team = Team { name: "Nyx FC", wins: 8, losses: 2 } print_ranking(s) // Student Alice: 95 points print_ranking(t) // Team Nyx FC: 24 points }
Exercises
- Define a trait
Areawith methodfn area(self) -> int. Implement it forCircle(radius) andRectangle(width, height). Write a function that prints the area of anyAreaimplementor.
- Define a trait
Printablewithfn display(self) -> String. Implement it for three different structs of your choice.
- Create a
struct Counter { value: int }with animplblock that has methods:increment(self) -> Counter,decrement(self) -> Counter,is_zero(self) -> bool.
- Define traits
Named(withfn name(self) -> String) andAged(withfn age(self) -> int). Implement both for aPersonstruct.
- Build a mini animal kingdom: trait
Animalwithfn speak(self) -> Stringandfn legs(self) -> int. Implement it forDog,Cat, andSpider. Print a report for each.
Summary
- A
traitdefines a set of methods a type must implement. impl Trait for Typeprovides the implementation.impl Typeadds methods directly without a trait.- Methods use
selfas the first parameter and are called with dot syntax. - Trait bounds (
) let functions accept any type with that trait. - One type can implement multiple traits.
- Traits separate "what" (interface) from "how" (implementation).
Next chapter: Generics →