Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
192: Supertraits
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::Debugandstd::default::Defaultas supertraits forConfig. - Add a method to
Configcalledsummarizethat prints the debug representation of the config and a custom "Config Summary" message. - Create a struct
ServerConfigwith a fieldport: u16. - Implement
Debug,Default, andConfigforServerConfig. - In
main, instantiateServerConfigusingServerConfig::default()and callsummarize().
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.
There are no comments for now.