394: Supertrait Pattern
Tutorial Video
Text description (accessibility)
This video demonstrates the "394: Supertrait Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Some traits only make sense when combined with others. Key difference from OCaml: 1. **Declaration**: Rust uses `: Supertrait` syntax in the trait definition; OCaml uses `include Sig` in module types or class `inherit` in objects.
Tutorial
The Problem
Some traits only make sense when combined with others. A Printable type that can print itself both in debug and display forms requires both Debug and Display. Instead of requiring callers to write T: Debug + Display + Printable everywhere, supertraits express this: trait Printable: Debug + Display declares that any type implementing Printable must also implement Debug and Display. This reduces boilerplate at every call site and makes semantic groupings explicit in the trait system.
Supertraits appear throughout std: Copy: Clone, Eq: PartialEq, Ord: Eq + PartialOrd, and Error: Debug + Display. They are the mechanism for trait inheritance in Rust.
🎯 Learning Outcomes
User: Printable implies User: Debug + Display transitivelyCode Example
#![allow(clippy::all)]
//! Supertrait Pattern
use std::fmt::{Debug, Display};
pub trait Printable: Debug + Display {
fn print(&self) {
println!("Debug: {:?}, Display: {}", self, self);
}
}
pub trait Entity: Clone + Default {
fn id(&self) -> u64;
}
#[derive(Debug, Clone, Default)]
pub struct User {
pub id: u64,
pub name: String,
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "User({})", self.name)
}
}
impl Printable for User {}
impl Entity for User {
fn id(&self) -> u64 {
self.id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_printable() {
let u = User {
id: 1,
name: "Alice".into(),
};
assert!(format!("{:?}", u).contains("Alice"));
}
#[test]
fn test_display() {
let u = User {
id: 1,
name: "Bob".into(),
};
assert_eq!(format!("{}", u), "User(Bob)");
}
#[test]
fn test_entity() {
let u = User {
id: 42,
..Default::default()
};
assert_eq!(u.id(), 42);
}
#[test]
fn test_clone() {
let u = User {
id: 1,
name: "X".into(),
};
let u2 = u.clone();
assert_eq!(u2.id, 1);
}
}Key Differences
: Supertrait syntax in the trait definition; OCaml uses include Sig in module types or class inherit in objects.T: Printable implies T: Debug + Display; OCaml requires explicit inclusion in each signature.derive attributes automatically satisfy supertrait requirements (#[derive(Debug, Clone)]); OCaml uses deriving ppx extensions.OCaml Approach
OCaml achieves supertrait-like behavior through module signature inclusion: module type PRINTABLE = sig include DEBUG; include DISPLAY; val print : t -> unit end. An implementing module must provide all fields from all included signatures. First-class modules can be chained this way. OCaml's object system achieves inheritance differently via class inheritance with inherit, but this is less common in functional OCaml code.
Full Source
#![allow(clippy::all)]
//! Supertrait Pattern
use std::fmt::{Debug, Display};
pub trait Printable: Debug + Display {
fn print(&self) {
println!("Debug: {:?}, Display: {}", self, self);
}
}
pub trait Entity: Clone + Default {
fn id(&self) -> u64;
}
#[derive(Debug, Clone, Default)]
pub struct User {
pub id: u64,
pub name: String,
}
impl Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "User({})", self.name)
}
}
impl Printable for User {}
impl Entity for User {
fn id(&self) -> u64 {
self.id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_printable() {
let u = User {
id: 1,
name: "Alice".into(),
};
assert!(format!("{:?}", u).contains("Alice"));
}
#[test]
fn test_display() {
let u = User {
id: 1,
name: "Bob".into(),
};
assert_eq!(format!("{}", u), "User(Bob)");
}
#[test]
fn test_entity() {
let u = User {
id: 42,
..Default::default()
};
assert_eq!(u.id(), 42);
}
#[test]
fn test_clone() {
let u = User {
id: 1,
name: "X".into(),
};
let u2 = u.clone();
assert_eq!(u2.id, 1);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_printable() {
let u = User {
id: 1,
name: "Alice".into(),
};
assert!(format!("{:?}", u).contains("Alice"));
}
#[test]
fn test_display() {
let u = User {
id: 1,
name: "Bob".into(),
};
assert_eq!(format!("{}", u), "User(Bob)");
}
#[test]
fn test_entity() {
let u = User {
id: 42,
..Default::default()
};
assert_eq!(u.id(), 42);
}
#[test]
fn test_clone() {
let u = User {
id: 1,
name: "X".into(),
};
let u2 = u.clone();
assert_eq!(u2.id, 1);
}
}
Deep Comparison
OCaml vs Rust: 394-supertrait-pattern
Exercises
trait Alive with fn breathe() -> String, then trait Animal: Alive with fn speak() -> String, then trait Pet: Animal with a default fn cuddle() -> String. Implement for Dog and Cat.trait Sortable: Ord + Clone with a default method fn sort_copy(items: &[Self]) -> Vec<Self> that returns a sorted copy. Implement it for i32 and a custom Score newtype.trait Report: Display with a default fn print_report(&self) and fn to_file(&self, path: &str) -> std::io::Result<()> that writes self.to_string() to disk. Implement for a SalesReport struct.