ExamplesBy LevelBy TopicLearning Paths
394 Intermediate

394: Supertrait Pattern

Functional Programming

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

  • • Understand how supertraits encode trait inheritance in Rust's type system
  • • Learn that supertrait bounds apply everywhere the child trait is used
  • • See how default methods in supertraits can leverage the required supertrait bounds
  • • Understand how User: Printable implies User: Debug + Display transitively
  • • Learn to use supertraits to group commonly-combined trait requirements
  • Code 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

  • Declaration: Rust uses : Supertrait syntax in the trait definition; OCaml uses include Sig in module types or class inherit in objects.
  • Default methods: Rust supertrait default methods can call supertrait methods directly; OCaml functor default implementations use similar delegation.
  • Transitivity: Rust's bounds are transitive — a bound T: Printable implies T: Debug + Display; OCaml requires explicit inclusion in each signature.
  • Derive interaction: Rust's 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Animal hierarchy: Define 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.
  • Sortable group: Define 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.
  • Display-requiring trait: Create 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.
  • Open Source Repos