๐Ÿฆ€ Functional Rust

148: Sealed Traits

Difficulty: 3 Level: Intermediate A public trait that downstream crates cannot implement โ€” you control the closed set of types forever.

The Problem This Solves

Sometimes you want a public trait that users of your library can use but cannot implement. This matters for stability guarantees: if you publish a trait and anyone can implement it, adding a new method to that trait in the next release is a breaking change โ€” every external `impl` would fail to compile. If you seal the trait, you own all implementations and can add methods freely. It also matters for correctness. Some traits carry invariants that must hold for all implementations. If an external crate can add a rogue implementation, those invariants can be violated. Sealing the trait means you've verified every implementation in your codebase. The Rust language doesn't have a built-in `sealed` keyword, but the pattern is simple and idiomatic: define a private supertrait in a private module. External code can't name `private::Sealed`, so it can't write `impl private::Sealed for TheirType`. Since your public trait requires `private::Sealed` as a supertrait, external code can't implement your trait either. It's a privacy trick, but it works perfectly and the compiler enforces it.

The Intuition

Put a private supertrait behind a private module โ€” external code can't see it, so it can't implement it, so it can't implement your public trait.

How It Works in Rust

// private module โ€” not pub, not accessible from outside this crate
mod private {
 pub trait Sealed {}  // accessible inside this crate; not from outside
}

// Public trait โ€” but requires the private Sealed supertrait
pub trait Shape: private::Sealed {
 fn area(&self) -> f64;
 fn perimeter(&self) -> f64;
 fn name(&self) -> &'static str;
}

// Only types in THIS crate can implement Shape,
// because only they can see and implement private::Sealed.

pub struct Circle { pub radius: f64 }
pub struct Rectangle { pub width: f64, pub height: f64 }

// Step 1: Seal the type (in this crate only)
impl private::Sealed for Circle {}
impl private::Sealed for Rectangle {}

// Step 2: Implement the public trait
impl Shape for Circle {
 fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
 fn perimeter(&self) -> f64 { 2.0 * std::f64::consts::PI * self.radius }
 fn name(&self) -> &'static str { "circle" }
}

// From an external crate, this would fail:
// struct MyShape;
// impl crate::private::Sealed for MyShape {}  // ERROR: module `private` is private
// impl crate::Shape for MyShape { ... }       // ERROR: missing Sealed bound

// Generic functions work fine โ€” Shape is still public and usable
fn total_area(shapes: &[&dyn Shape]) -> f64 {
 shapes.iter().map(|s| s.area()).sum()
}

What This Unlocks

Key Differences

ConceptOCamlRust
Sealed module interfaceAbstract types / restricted module visibilityPrivate supertrait in private module
Adding methods laterSafe if module is abstractSafe if trait is sealed
External implementorsPrevented by module systemPrevented by private supertrait
Capability tokensAbstract tokens in restricted modulesPrivate-field unit struct, private constructor
// Sealed traits: a public trait that external crates cannot implement.
//
// The pattern uses a private supertrait in a private module.
// External code can *use* the trait but cannot add new `impl`s for it.
// This gives library authors control over which types implement a trait.

// โ”€โ”€ The sealed-trait pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

mod private {
    /// A marker trait in a private module.
    /// External code cannot name this trait, so it cannot impl Sealed for its types.
    pub trait Sealed {}
}

/// A public trait with a private supertrait โ€” effectively sealed.
/// Any type T that implements `Shape` must also implement `private::Sealed`,
/// but external crates cannot write `impl private::Sealed for MyType`.
pub trait Shape: private::Sealed {
    fn area(&self) -> f64;
    fn perimeter(&self) -> f64;
    fn name(&self) -> &'static str;

    fn describe(&self) -> String {
        format!(
            "{}: area={:.4}, perimeter={:.4}",
            self.name(),
            self.area(),
            self.perimeter()
        )
    }
}

// โ”€โ”€ Permitted implementors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Only types inside this crate can implement Shape, because only they can
// also implement `private::Sealed`.

#[derive(Debug, Clone)]
pub struct Circle {
    pub radius: f64,
}

#[derive(Debug, Clone)]
pub struct Rectangle {
    pub width: f64,
    pub height: f64,
}

#[derive(Debug, Clone)]
pub struct EquilateralTriangle {
    pub side: f64,
}

// Seal each type
impl private::Sealed for Circle {}
impl private::Sealed for Rectangle {}
impl private::Sealed for EquilateralTriangle {}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
    fn perimeter(&self) -> f64 {
        2.0 * std::f64::consts::PI * self.radius
    }
    fn name(&self) -> &'static str { "circle" }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 { self.width * self.height }
    fn perimeter(&self) -> f64 { 2.0 * (self.width + self.height) }
    fn name(&self) -> &'static str { "rectangle" }
}

impl Shape for EquilateralTriangle {
    fn area(&self) -> f64 {
        let s = self.side;
        (3.0_f64.sqrt() / 4.0) * s * s
    }
    fn perimeter(&self) -> f64 { 3.0 * self.side }
    fn name(&self) -> &'static str { "equilateral-triangle" }
}

// โ”€โ”€ Sealed token pattern โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// An alternative use: pass a sealed token as a proof that the caller is
// in the "permitted" set โ€” like a capability token.

mod token_private {
    pub trait TokenSeal {}
}

/// A token that only this crate can construct.
pub struct AdminToken(());

impl token_private::TokenSeal for AdminToken {}

pub trait AdminAction: token_private::TokenSeal {
    /// Only callable with an AdminToken โ€” and only we can make AdminTokens.
    fn execute(&self, _token: &AdminToken) -> String;
}

impl AdminAction for AdminToken {
    fn execute(&self, _token: &AdminToken) -> String {
        "admin action executed".to_string()
    }
}

impl AdminToken {
    /// The only way to create an AdminToken โ€” validates credentials.
    pub fn authenticate(password: &str) -> Option<Self> {
        if password == "hunter2" {
            Some(AdminToken(()))
        } else {
            None
        }
    }
}

// โ”€โ”€ Generic function over sealed shapes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

fn total_area(shapes: &[&dyn Shape]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn largest<'a>(shapes: &[&'a dyn Shape]) -> Option<&'a dyn Shape> {
    shapes.iter()
        .max_by(|a, b| a.area().partial_cmp(&b.area()).unwrap())
        .copied()
}

fn main() {
    let c = Circle { radius: 5.0 };
    let r = Rectangle { width: 3.0, height: 4.0 };
    let t = EquilateralTriangle { side: 6.0 };

    let shapes: Vec<&dyn Shape> = vec![&c, &r, &t];

    println!("Shapes:");
    for s in &shapes {
        println!("  {}", s.describe());
    }

    println!("Total area: {:.4}", total_area(&shapes));
    if let Some(big) = largest(&shapes) {
        println!("Largest: {}", big.name());
    }

    // Sealed token
    match AdminToken::authenticate("hunter2") {
        Some(token) => println!("\n{}", token.execute(&token)),
        None        => println!("auth failed"),
    }
    match AdminToken::authenticate("wrong") {
        None => println!("Bad password rejected"),
        Some(_) => println!("unexpected"),
    }

    // Note: the following would NOT compile from an external crate:
    //   struct MyShape;
    //   impl private::Sealed for MyShape {}  // error: `private` is private
    //   impl Shape for MyShape { ... }
    println!("\nSealed trait pattern works โ€” external impls are impossible.");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_circle_area() {
        let c = Circle { radius: 1.0 };
        assert!((c.area() - std::f64::consts::PI).abs() < 1e-10);
    }

    #[test]
    fn test_rectangle() {
        let r = Rectangle { width: 3.0, height: 4.0 };
        assert_eq!(r.area(), 12.0);
        assert_eq!(r.perimeter(), 14.0);
    }

    #[test]
    fn test_triangle() {
        let t = EquilateralTriangle { side: 2.0 };
        let expected = (3.0_f64.sqrt() / 4.0) * 4.0;
        assert!((t.area() - expected).abs() < 1e-10);
        assert_eq!(t.perimeter(), 6.0);
    }

    #[test]
    fn test_total_area() {
        let r = Rectangle { width: 4.0, height: 5.0 };
        let r2 = Rectangle { width: 2.0, height: 3.0 };
        let shapes: Vec<&dyn Shape> = vec![&r, &r2];
        assert!((total_area(&shapes) - 26.0).abs() < 1e-10);
    }

    #[test]
    fn test_admin_token() {
        assert!(AdminToken::authenticate("hunter2").is_some());
        assert!(AdminToken::authenticate("bad").is_none());
    }

    #[test]
    fn test_describe() {
        let c = Circle { radius: 1.0 };
        let desc = c.describe();
        assert!(desc.starts_with("circle:"));
    }
}
(* OCaml doesn't have sealed traits natively, but we can simulate them
   with module types + private constructors. The pattern restricts who
   can create instances of a type. *)

(* Shape hierarchy: only shapes defined here can implement Shape *)
module type SHAPE = sig
  type t
  val area     : t -> float
  val perimeter: t -> float
  val name     : string
end

module Circle : SHAPE = struct
  type t = { radius: float }
  let area     c = Float.pi *. c.radius *. c.radius
  let perimeter c = 2. *. Float.pi *. c.radius
  let name = "circle"
end

module Rect : SHAPE = struct
  type t = { w: float; h: float }
  let area     r = r.w *. r.h
  let perimeter r = 2. *. (r.w +. r.h)
  let name = "rectangle"
end

(* Existential wrapper for dispatch *)
type shape = Shape : (module SHAPE with type t = 'a) * 'a -> shape

let circle r = Shape ((module Circle), { Circle.radius = r })
let rect w h = Shape ((module Rect),   { Rect.w = w; Rect.h = h })

let area (Shape ((module S), s)) = S.area s
let perimeter (Shape ((module S), s)) = S.perimeter s
let name (Shape ((module S), _)) = S.name

let () =
  let shapes = [circle 5.; rect 3. 4.; circle 1.] in
  List.iter (fun s ->
    Printf.printf "%s: area=%.2f perimeter=%.2f\n"
      (name s) (area s) (perimeter s)
  ) shapes

๐Ÿ“Š Detailed Comparison

Comparison: Sealed Traits

OCaml

๐Ÿช Show OCaml equivalent
(* Sealed via abstract type in signature *)
module type TOKEN = sig
type t    (* abstract โ€” can't create outside *)
val create : string -> t
val verify : t -> bool
end

Rust

// Sealed trait pattern
mod sealed { pub trait Sealed {} }

pub trait Permission: sealed::Sealed {
 fn level(&self) -> u8;
}

// Only types in this crate can impl Sealed, so only we can impl Permission
impl sealed::Sealed for Read {}
impl Permission for Read { fn level(&self) -> u8 { 1 } }