Introduction
Supertraits let you build trait hierarchies: requiring that a type implements one trait before it can implement another. Blanket implementations extend traits to all types matching a bound. Together, they form the backbone of Rust's composable trait system.
Key Concepts
- Supertrait: A trait required by another trait, declared with
trait A: B { }. ImplementingArequires first implementingB. - Blanket Implementation: An
impl<T: Bound> Trait for Tthat applies to all types matching the bound. - Trait Coherence: The orphan rule ensures that blanket impls do not conflict with local impls.
Real World Context
Error: Debug + Display ensures every error type is printable. The blanket impl impl<T: Display> ToString for T gives every displayable type a .to_string() method for free.
Deep Dive
Supertraits
Declare that implementing one trait requires another:
rustuse std::fmt::{Debug, Display}; trait Error: Debug + Display { fn source(&self) -> Option<&(dyn Error + 'static)> { None } } // To implement Error, you MUST implement Debug + Display #[derive(Debug)] struct MyError(String); impl Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}", self.0) } } impl Error for MyError {} // OK: Debug + Display are both implemented
Blanket Implementations
Implement a trait for all types matching a bound:
rustimpl<T: Display> ToString for T { fn to_string(&self) -> String { format!("{self}") } } // Now 42.to_string(), 3.14.to_string() etc. all work
The From/Into blanket impl is another classic:
rustimpl<T, U> Into<U> for T where U: From<T> { fn into(self) -> U { U::from(self) } } // Implement From<A> for B, and you get Into<B> for A for free
Common Pitfalls
- Orphan rule conflicts with blanket impls — You cannot implement a foreign trait for a foreign type. Blanket impls in upstream crates can prevent you from implementing traits on your types.
- Circular supertrait requirements — Traits cannot form cycles. If
A: BandB: A, the compiler rejects it.
Best Practices
- Keep supertrait hierarchies shallow — Deep hierarchies force implementors to satisfy many traits. Prefer composition over deep inheritance.
- Provide blanket impls for free functionality — If your trait has a method that can be derived from a simpler trait, provide a blanket impl.
Summary
- Supertraits enforce trait dependencies:
trait A: Bmeans implementing A requires B. - Blanket impls provide automatic implementations for all types matching a bound.
Display→ToStringandFrom→Intoare canonical blanket impl examples.- Keep trait hierarchies shallow and composable.
Code Examples
rust
use std::fmt::Debug;
// Custom supertrait + blanket implementation
trait Loggable: Debug {
fn log(&self) {
println!("[LOG] {:?}", self);
}
}
// Blanket impl: everything that is Debug becomes Loggable
impl<T: Debug> Loggable for T {}
fn main() {
42.log(); // [LOG] 42
"hello".log(); // [LOG] "hello"
vec![1, 2, 3].log(); // [LOG] [1, 2, 3]
Some(true).log(); // [LOG] Some(true)
}