ExamplesBy LevelBy TopicLearning Paths
387 Intermediate

387: Sealed Trait Pattern

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "387: Sealed Trait Pattern" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Rust's trait system is open by default — any crate can implement any trait for any type (subject to orphan rules). Key difference from OCaml: 1. **Mechanism**: Rust uses a private supertrait in a private module; OCaml uses abstract types or private type aliases in module signatures.

Tutorial

The Problem

Rust's trait system is open by default — any crate can implement any trait for any type (subject to orphan rules). Sometimes library authors want to prevent external implementations: a Token trait whose implementors are exactly the types the library defines, ensuring exhaustive handling and preventing downstream breakage when new variants are added. The sealed trait pattern uses a private Sealed supertrait to enforce this: only types that implement private::Sealed can implement the public trait, and private::Sealed cannot be named by external code.

This pattern appears in tokio's Sealed trait for internal types, bytes::Buf, futures::Stream, and many API-stability-sensitive libraries.

🎯 Learning Outcomes

  • • Understand why you might want to prevent external implementations of a public trait
  • • Learn how the mod private + Sealed supertrait pattern enforces this in Rust
  • • See how public trait bounds on a private supertrait create an unimplementable interface for external users
  • • Understand the API stability guarantee sealed traits provide
  • • Learn the difference between sealed traits (prevent impl) and private traits (prevent use)
  • Code Example

    #![allow(clippy::all)]
    //! Sealed Trait Pattern
    
    mod private {
        pub trait Sealed {}
    }
    
    pub trait Token: private::Sealed {
        fn value(&self) -> String;
        fn token_type(&self) -> &'static str;
    }
    
    pub struct Identifier(pub String);
    pub struct Number(pub i64);
    
    impl private::Sealed for Identifier {}
    impl private::Sealed for Number {}
    
    impl Token for Identifier {
        fn value(&self) -> String {
            self.0.clone()
        }
        fn token_type(&self) -> &'static str {
            "identifier"
        }
    }
    
    impl Token for Number {
        fn value(&self) -> String {
            self.0.to_string()
        }
        fn token_type(&self) -> &'static str {
            "number"
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_identifier() {
            let id = Identifier("foo".into());
            assert_eq!(id.value(), "foo");
            assert_eq!(id.token_type(), "identifier");
        }
        #[test]
        fn test_number() {
            let n = Number(42);
            assert_eq!(n.value(), "42");
            assert_eq!(n.token_type(), "number");
        }
        #[test]
        fn test_trait_object() {
            let tokens: Vec<Box<dyn Token>> =
                vec![Box::new(Identifier("x".into())), Box::new(Number(1))];
            assert_eq!(tokens.len(), 2);
        }
    }

    Key Differences

  • Mechanism: Rust uses a private supertrait in a private module; OCaml uses abstract types or private type aliases in module signatures.
  • Error quality: Rust's error for attempting to implement a sealed trait mentions the private Sealed bound; OCaml's error is a "type not accessible" module error.
  • Use vs. impl: Rust's sealed traits are fully usable as bounds and in dyn Trait positions; OCaml's abstract types can be used but not extended.
  • Documentation: Rust sealed traits can document the pattern explicitly in rustdoc; OCaml hides the mechanism entirely in the .mli file.
  • OCaml Approach

    OCaml achieves sealed modules through the module system. A private module signature can expose a type but hide its constructors: module type SEALED = sig type t = private Foo | Bar end. External code can pattern-match exhaustively but cannot construct new values. For trait-like sealing, OCaml uses abstract types in signatures where the concrete representation is hidden.

    Full Source

    #![allow(clippy::all)]
    //! Sealed Trait Pattern
    
    mod private {
        pub trait Sealed {}
    }
    
    pub trait Token: private::Sealed {
        fn value(&self) -> String;
        fn token_type(&self) -> &'static str;
    }
    
    pub struct Identifier(pub String);
    pub struct Number(pub i64);
    
    impl private::Sealed for Identifier {}
    impl private::Sealed for Number {}
    
    impl Token for Identifier {
        fn value(&self) -> String {
            self.0.clone()
        }
        fn token_type(&self) -> &'static str {
            "identifier"
        }
    }
    
    impl Token for Number {
        fn value(&self) -> String {
            self.0.to_string()
        }
        fn token_type(&self) -> &'static str {
            "number"
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_identifier() {
            let id = Identifier("foo".into());
            assert_eq!(id.value(), "foo");
            assert_eq!(id.token_type(), "identifier");
        }
        #[test]
        fn test_number() {
            let n = Number(42);
            assert_eq!(n.value(), "42");
            assert_eq!(n.token_type(), "number");
        }
        #[test]
        fn test_trait_object() {
            let tokens: Vec<Box<dyn Token>> =
                vec![Box::new(Identifier("x".into())), Box::new(Number(1))];
            assert_eq!(tokens.len(), 2);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_identifier() {
            let id = Identifier("foo".into());
            assert_eq!(id.value(), "foo");
            assert_eq!(id.token_type(), "identifier");
        }
        #[test]
        fn test_number() {
            let n = Number(42);
            assert_eq!(n.value(), "42");
            assert_eq!(n.token_type(), "number");
        }
        #[test]
        fn test_trait_object() {
            let tokens: Vec<Box<dyn Token>> =
                vec![Box::new(Identifier("x".into())), Box::new(Number(1))];
            assert_eq!(tokens.len(), 2);
        }
    }

    Deep Comparison

    OCaml vs Rust: 387-sealed-trait-pattern

    Exercises

  • Sealed codec: Define a Codec sealed trait with encode and decode methods. Implement it for JsonCodec and BinaryCodec in the same crate. Write tests proving that a downstream crate cannot add a XmlCodec implementation.
  • Visitor pattern with sealing: Implement a sealed Visitor trait for an AST node hierarchy. Ensure all node types implement the visitor contract while preventing external AST node additions.
  • Version-gated unsealing: Design a library that starts with a sealed trait but plans to unseal it in a future version. Write a feature-flag-based migration path using #[cfg(feature = "unstable")] to expose the sealing mechanism for pre-release testing.
  • Open Source Repos