Generics, Traits and Lifetimes · about 45 min

Traits: Shared Behavior

Why this matters

Generic functions need to know what they can do with a type: compare it, print it, compute its area. Different types also often share behavior: circles and rectangles both have an area; integers, strings and your own structs can all be printed. A trait names a set of methods that types can implement. Traits are Rust's interfaces, and they are everywhere: Display, Debug, Clone, Iterator, From, Drop are all traits you have already met. This lesson shows how to define and implement them; the next shows how to write code that works with any type implementing a trait.

Defining a trait

trait Describe {
    fn name(&self) -> String;
    fn legs(&self) -> u32;
}

struct Dog;
struct Bird {
    species: String,
}

impl Describe for Dog {
    fn name(&self) -> String {
        String::from("dog")
    }
    fn legs(&self) -> u32 {
        4
    }
}

impl Describe for Bird {
    fn name(&self) -> String {
        format!("bird ({})", self.species)
    }
    fn legs(&self) -> u32 {
        2
    }
}

fn main() {
    let d = Dog;
    let b = Bird { species: String::from("robin") };
    println!("{} has {} legs", d.name(), d.legs());
    println!("{} has {} legs", b.name(), b.legs());
}
dog has 4 legs
bird (robin) has 2 legs

Forgetting a method is an error that lists what is missing:

trait Describe {
    fn name(&self) -> String;
    fn legs(&self) -> u32;
}

struct Snake;

impl Describe for Snake {
    fn name(&self) -> String {
        String::from("snake")
    }
}

fn main() {
    println!("{}", Snake.name());
}
error[E0046]: not all trait items implemented, missing: `legs`
 --> main.rs:8:1
  |
3 |     fn legs(&self) -> u32;
  |     ---------------------- `legs` from trait
...
8 | impl Describe for Snake {
  | ^^^^^^^^^^^^^^^^^^^^^^^ missing `legs` in implementation

Default methods

A trait can provide default implementations, which types get for free and may override:

trait Describe {
    fn name(&self) -> String;
    fn legs(&self) -> u32;

    fn summary(&self) -> String {
        format!("a {} with {} legs", self.name(), self.legs())
    }
}

struct Dog;
struct Fish;

impl Describe for Dog {
    fn name(&self) -> String { String::from("dog") }
    fn legs(&self) -> u32 { 4 }
}

impl Describe for Fish {
    fn name(&self) -> String { String::from("fish") }
    fn legs(&self) -> u32 { 0 }
    fn summary(&self) -> String {
        String::from("a fish, which swims instead")
    }
}

fn main() {
    println!("{}", Dog.summary());
    println!("{}", Fish.summary());
}
a dog with 4 legs
a fish, which swims instead

Default methods can call the required ones. This pattern is powerful: the standard Iterator trait has one required method (next) and dozens of default methods (map, filter, sum, count...) built on it. Implement next for your type and you get all of them (module 11).

Implementing standard traits

Your types become first-class citizens by implementing standard traits. Display (what {} uses) is the most common one written by hand:

use std::fmt;

struct Point {
    x: i32,
    y: i32,
}

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 3, y: -1 };
    println!("p = {p}");
    let s = p.to_string();
    println!("{s:>12}|");
}
p = (3, -1)
     (3, -1)|

Implementing Display also gives you to_string() for free: the standard library implements another trait, ToString, for every type that implements Display. This is called a blanket implementation, and it is another way traits compose. (Width and alignment like {:>12} apply to the result of to_string; inside fmt, f.pad(...) would honor them directly.)

Traits with associated functions and constants

A trait can require functions without self (called as Type::function()) and constants:

trait Shape {
    const SIDES: u32;
    fn unit() -> Self;
    fn area(&self) -> f64;
}

struct Square(f64);

impl Shape for Square {
    const SIDES: u32 = 4;
    fn unit() -> Self {
        Square(1.0)
    }
    fn area(&self) -> f64 {
        self.0 * self.0
    }
}

fn main() {
    let s = Square::unit();
    println!("{} sides, area {}", Square::SIDES, s.area());
}
4 sides, area 1

Self in a trait means "the implementing type". Default::default() and From::from() are standard examples of trait functions without self.

Where impls may live: the orphan rule

You can implement:

but not a foreign trait for a foreign type (Display for Vec<i32>). Otherwise two crates could provide conflicting implementations. The workaround is a newtype wrapper (struct Report(Vec<i32>)) for which you can implement anything.

trait Double {
    fn double(&self) -> Self;
}

impl Double for i32 {
    fn double(&self) -> Self {
        self * 2
    }
}

impl Double for String {
    fn double(&self) -> Self {
        self.repeat(2)
    }
}

fn main() {
    println!("{} {}", 21.double(), String::from("ab").double());
}
42 abab

Extending existing types with your own traits ("extension traits") is a common, legitimate pattern. Note that the trait must be in scope (used) wherever its methods are called.

Common mistakes

Recap

Going further: read the Rust Book chapter 10.2, "Traits: Defining Shared Behavior". Then implement Display for the Shape enum from module 7.

Exercises

Exercise 1 · write code

Define and implement a trait

Define a trait Shape with two required methods, fn area(&self) -> f64 and fn name(&self) -> String, and a default method fn describe(&self) -> String returning "NAME with area A" (area with 2 decimals). Implement Shape for struct Circle { r: f64 } (name "circle") and struct Rect { w: f64, h: f64 } (name "rectangle"). Use std::f64::consts::PI.

The given main prints:

circle with area 3.14
rectangle with area 6.00

File: main.rs

Starting point

struct Circle {
    r: f64,
}

struct Rect {
    w: f64,
    h: f64,
}

fn main() {
    println!("{}", Circle { r: 1.0 }.describe());
    println!("{}", Rect { w: 2.0, h: 3.0 }.describe());
}

Example

expected output

circle with area 3.14
rectangle with area 6.00
Hint 1

In the trait, required methods end with ;; the default method has a body that calls self.name() and self.area().

Hint 2

impl Shape for Circle { fn area(&self) -> f64 { ... } fn name(&self) -> String { ... } }: no need to write describe.

Reference solution
use std::f64::consts::PI;

trait Shape {
    fn area(&self) -> f64;
    fn name(&self) -> String;
    fn describe(&self) -> String {
        format!("{} with area {:.2}", self.name(), self.area())
    }
}

struct Circle {
    r: f64,
}

struct Rect {
    w: f64,
    h: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        PI * self.r * self.r
    }
    fn name(&self) -> String {
        String::from("circle")
    }
}

impl Shape for Rect {
    fn area(&self) -> f64 {
        self.w * self.h
    }
    fn name(&self) -> String {
        String::from("rectangle")
    }
}

fn main() {
    println!("{}", Circle { r: 1.0 }.describe());
    println!("{}", Rect { w: 2.0, h: 3.0 }.describe());
}

Exercise 2 · write code

Implement Display

Implement std::fmt::Display for struct Money { cents: i64 } so that it prints as dollars with a sign for negative amounts and exactly two decimals: $12.05, -$0.50, $0.00. The given main reads amounts in cents from stdin and prints each with {}:

$12.05
-$0.50
$0.00

File: main.rs

Starting point

use std::fmt;
use std::io;

struct Money {
    cents: i64,
}

fn main() {
    for line in io::stdin().lines() {
        let cents: i64 = line.unwrap().trim().parse().unwrap();
        println!("{}", Money { cents });
    }
}

Example

input

1205
-50
0

expected output

$12.05
-$0.50
$0.00

Example

input

-123456
7

expected output

-$1234.56
$0.07
Hint 1

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

Hint 2

Work with the absolute value: dollars abs / 100, cents abs % 100 with {:02}, and put the sign first.

Reference solution
use std::fmt;
use std::io;

struct Money {
    cents: i64,
}

impl fmt::Display for Money {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let sign = if self.cents < 0 { "-" } else { "" };
        let abs = self.cents.abs();
        write!(f, "{}${}.{:02}", sign, abs / 100, abs % 100)
    }
}

fn main() {
    for line in io::stdin().lines() {
        let cents: i64 = line.unwrap().trim().parse().unwrap();
        println!("{}", Money { cents });
    }
}

Exercise 3 · quiz

Which impls are allowed?

In your own crate, which of these trait implementations are allowed? Select all that apply. (Summary and Tweet are defined in your crate; Display and Vec come from the standard library.)

Answer
  • impl Summary for Tweet

  • impl Summary for Vec<i32>

  • impl std::fmt::Display for Tweet

The orphan rule: you may implement a trait for a type only if the trait or the type (or both) is defined in your crate. Implementing a foreign trait for a foreign type is forbidden, because two crates could both do it and the compiler could not choose. To display a Vec your way, wrap it in a newtype (struct List(Vec<i32>)) and implement Display for that.