RustArena

Step 3 of 7

Supertraits and fully qualified syntax

Supertraits: "to implement this, you must also implement that"

Sometimes a trait only makes sense for types that already have another ability. An alert must be printable, so Alert can require Display:

use std::fmt;

trait Alert: fmt::Display {
    fn level(&self) -> u8;

    fn banner(&self) -> String {
        let tag = if self.level() >= 3 { "CRITICAL" } else { "notice" };
        format!("[{tag}] {self}")
    }
}

struct DiskFull {
    mount: String,
    percent: u8,
}

impl fmt::Display for DiskFull {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} is {}% full", self.mount, self.percent)
    }
}

impl Alert for DiskFull {
    fn level(&self) -> u8 {
        if self.percent >= 95 { 3 } else { 1 }
    }
}

fn main() {
    let var = DiskFull { mount: "/var".to_string(), percent: 97 };
    let home = DiskFull { mount: "/home".to_string(), percent: 80 };
    println!("{}", var.banner());
    println!("{}", home.banner());
}
[CRITICAL] /var is 97% full
[notice] /home is 80% full

trait Alert: fmt::Display makes Display a supertrait of Alert. Two things follow: every type that implements Alert must also implement Display, and code inside Alert (like the default banner) may use {self} because it's guaranteed to work. A function taking T: Alert can print T too, without also writing T: Display.

It looks like inheritance, but it isn't: no fields or method bodies are inherited. It's a requirement, checked by the compiler. std is full of them: Copy: Clone, Eq: PartialEq, Ord: Eq + PartialOrd, and std::error::Error: Debug + Display (that's why your error types in the custom-errors module needed both).

use std::fmt::Display;

trait Loggable: Display {
    fn log_line(&self) -> String {
        format!("[LOG] {self}")
    }
}

struct Ticket {
    id: u32,
}

impl Loggable for Ticket {}

fn main() {
    let t = Ticket { id: 1 };
    println!("{}", t.id);
}
error[E0277]: `Ticket` doesn't implement `std::fmt::Display`

Two traits, one method name

Nothing stops two traits from having methods with the same name. When one type implements both, a plain method call is ambiguous, so you name the trait. This is fully qualified syntax.

trait Employee {
    fn id(&self) -> String;
    fn role() -> &'static str;
}

trait Customer {
    fn id(&self) -> String;
    fn role() -> &'static str;
}

struct Person {
    staff_no: u32,
    account: u32,
}

impl Employee for Person {
    fn id(&self) -> String {
        format!("E-{}", self.staff_no)
    }
    fn role() -> &'static str {
        "employee"
    }
}

impl Customer for Person {
    fn id(&self) -> String {
        format!("C-{}", self.account)
    }
    fn role() -> &'static str {
        "customer"
    }
}

fn main() {
    let ana = Person { staff_no: 7, account: 1042 };
    println!("{}", Employee::id(&ana));
    println!("{}", Customer::id(&ana));
    println!("{}", <Person as Employee>::role());
    println!("{}", <Person as Customer>::role());
}
E-7
C-1042
employee
customer

Common mistakes

trait Named {
    fn name() -> String;
}

struct Crab;

impl Named for Crab {
    fn name() -> String {
        "Ferris".to_string()
    }
}

fn main() {
    println!("{}", Named::name());
}
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type

Your task

Distance implements both Metric and Imperial, which have the same two names: unit() (no self) and value(&self). Write describe, which returns both measurements with one decimal place, like 12.0 m = 39.4 ft. Get the units from the traits with fully qualified syntax rather than typing "m" and "ft".

Challenge 2

Ferris is both a pilot and a chef. Fill in the blanks to call each trait's version, so the program prints:

Captain Ferris on the bridge
Chef Ferris in the kitchen
pilot and chef

Challenge 3

Write a trait with two supertraits. Sellable must require both Priced (given) and fmt::Display, and have a default method fn receipt_line(&self) -> String that returns the item's display text, a colon, and its price in dollars, like Rust in Action: $39.99.

Then make Book sellable: implement Display (shows the title), Priced (returns price_cents), and an empty impl Sellable for Book {}. Finally write fn total_cents<T: Sellable>(items: &[T]) -> u32, which only needs the Sellable bound to add up the prices.

Previous: Operator traits and default type parameters Next: Blanket impls and extension traits