387: Sealed Trait Pattern
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
mod private + Sealed supertrait pattern enforces this in RustCode 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
Sealed bound; OCaml's error is a "type not accessible" module error.dyn Trait positions; OCaml's abstract types can be used but not extended..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);
}
}#[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
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 trait for an AST node hierarchy. Ensure all node types implement the visitor contract while preventing external AST node additions.#[cfg(feature = "unstable")] to expose the sealing mechanism for pre-release testing.