Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this trip up a lot of developers moving from Java or C# into Rust. You try to create a trait that "extends" another trait, and suddenly the compiler starts screaming about trait bounds that you *thought* you had already handled. Let's look at a scenario where this usually happens.

Imagine you're building a system to handle different types of log messages. You want a trait called Loggable that provides a formatted output for your logging system. You figure, "Well, if it's loggable, it should probably be printable using the standard Display trait."

trait Loggable {
    fn log_level(&self) -> &str;

    fn print_log(&self) {
        // We want to use the Display implementation here
        println!("[{}] {}", self.log_level(), self); 
    }
}

struct ErrorLog {
    message: String,
}

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

impl Loggable for ErrorLog {
    fn log_level(&self) -> &str { "ERROR" }
}

fn main() {
    let err = ErrorLog { message: "Disk full".to_string() };
    err.print_log();
}

The "trait bound not satisfied" headache

If you try to compile this, Rust is going to stop you dead in your tracks. The error will look something like: the trait bound `Self: std::fmt::Display` is not satisfied.

Now, you're probably looking at the code and thinking, "But I did implement Display for ErrorLog!" You're right, you did. But the compiler isn't looking at ErrorLog right now—it's looking at the Loggable trait definition.

Inside print_log, you're trying to pass self into println!. The println! macro requires that whatever it prints implements std::fmt::Display. However, as far as the Loggable trait is concerned, self is just... some type that implements Loggable. It has no guarantee that the type also implements Display. Even if ErrorLog does, some other type implementing Loggable might not.

Enforcing dependencies with Supertraits

To fix this, we need to tell Rust that Loggable cannot be implemented by any type unless that type already implements Display. This is where supertraits come in.

We do this by adding a colon after the trait name. It looks like this:

trait Loggable: std::fmt::Display {
    fn log_level(&self) -> &str;

    fn print_log(&self) {
        println!("[{}] {}", self.log_level(), self); 
    }
}

By writing trait Loggable: std::fmt::Display, you've established a supertrait relationship. You aren't "inheriting" methods in the way you do in OOP; rather, you are adding a requirement. You're telling the compiler: "To be a Loggable, you must first be a Display."

Now, the code compiles perfectly. When the compiler looks at print_log, it sees that self is a Loggable, and since Loggable requires Display, it knows for a fact that self can be printed.

I like to think of supertraits as a prerequisite list. If I'm writing a trait for AdvancedCalculations, I might require Num. If I'm writing a trait for DatabaseRecord, I might require Serialize and Deserialize. It keeps your trait logic clean because you can rely on the functionality of the supertrait without having to write generic bounds on every single method inside the trait.




📋 Practical Task

Implementing a Validated Configuration System

You are building a configuration system where every configuration struct must be able to provide a default value (via Default) and be printable for debugging purposes (via Debug). Your goal is to create a trait that enforces these requirements.

  • Create a trait named Config.
  • Set std::fmt::Debug and std::default::Default as supertraits for Config.
  • Add a method to Config called summarize that prints the debug representation of the config and a custom "Config Summary" message.
  • Create a struct ServerConfig with a field port: u16.
  • Implement Debug, Default, and Config for ServerConfig.
  • In main, instantiate ServerConfig using ServerConfig::default() and call summarize().

Challenge: Try removing the #[derive(Debug)] or #[derive(Default)] from ServerConfig after you've finished. Observe how the compiler prevents you from implementing Config because the supertrait requirements are no longer met.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.