Table of Contents

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

  1. Define a trait Area with method fn area(self) -> int. Implement it for Circle (radius) and Rectangle (width, height). Write a function that prints the area of any Area implementor.
  1. Define a trait Printable with fn display(self) -> String. Implement it for three different structs of your choice.
  1. Create a struct Counter { value: int } with an impl block that has methods: increment(self) -> Counter, decrement(self) -> Counter, is_zero(self) -> bool.
  1. Define traits Named (with fn name(self) -> String) and Aged (with fn age(self) -> int). Implement both for a Person struct.
  1. Build a mini animal kingdom: trait Animal with fn speak(self) -> String and fn legs(self) -> int. Implement it for Dog, Cat, and Spider. Print a report for each.

Summary

Next chapter: Generics →

← Previous: Enums and pattern matching Next: Generics →