ExamplesBy LevelBy TopicLearning Paths
182 Expert

Existential Types via Box<dyn Trait>

Functional Programming

Tutorial

The Problem

An existential type packs a value together with proof that it satisfies an interface, erasing the concrete type. The consumer of an existential value can use the interface but cannot recover the original type. This enables heterogeneous collections (a Vec of different types all implementing the same trait), plugin architectures, and open-ended extension points. Rust's Box<dyn Trait> is the primary existential mechanism, paralleling OCaml's GADT-based existential encoding.

🎯 Learning Outcomes

  • • Understand existential types as "there exists a type satisfying this interface"
  • • Build heterogeneous collections with Box<dyn Display> and custom traits
  • • Implement the closure-based existential pattern for packing values with their operations
  • • See how existentials enable extension without recompilation (open-world assumption)
  • Code Example

    let items: Vec<Box<dyn fmt::Display>> = vec![Box::new(42), Box::new("hello")];
    let results: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();

    Key Differences

  • GADT vs. closure: OCaml's GADT explicitly packs (value, function); Rust's closure-based approach bundles them in the captured environment — equivalent but differently structured.
  • Vtable vs. closure: Rust's Box<dyn Trait> uses a vtable; the closure approach creates an independent dispatch mechanism — useful when the "trait" is just one function.
  • Type recovery: Neither OCaml's Show existential nor Rust's Box<dyn Any> allows recovering the concrete type in the existential position; Box<dyn Any> is a different (universally typed) erasure.
  • Collection use: Both Vec<showable> in OCaml and Vec<Box<dyn Display>> in Rust are idiomatic for heterogeneous collections.
  • OCaml Approach

    OCaml encodes existentials via GADTs:

    type showable = Show : 'a * ('a -> string) -> showable
    let show (Show (x, f)) = f x
    let showables = [Show (42, string_of_int); Show ("hello", Fun.id)]
    

    The Show constructor packs the value x: 'a and its show function f: 'a -> string, erasing 'a in showable. OCaml's encoding is more transparent than Rust's closure-based approach — the packed function is explicit.

    Full Source

    #![allow(clippy::all)]
    // Example 182: Existential Types
    // Hide the concrete type while retaining ability to use it
    
    use std::fmt;
    
    // === Approach 1: Box<dyn Trait> — Rust's native existential ===
    
    fn make_showables() -> Vec<Box<dyn fmt::Display>> {
        vec![
            Box::new(42),
            Box::new("hello"),
            Box::new(3.14),
            Box::new(true),
        ]
    }
    
    // === Approach 2: Custom existential with closure (like OCaml GADT) ===
    
    struct Showable {
        show_fn: Box<dyn Fn() -> String>,
    }
    
    impl Showable {
        fn new<T: 'static>(value: T, to_string: fn(&T) -> String) -> Self {
            Showable {
                show_fn: Box::new(move || to_string(&value)),
            }
        }
    
        fn show(&self) -> String {
            (self.show_fn)()
        }
    }
    
    // === Approach 3: Existential with comparison ===
    
    struct Comparable {
        compare_fn: Box<dyn Fn() -> std::cmp::Ordering>,
        describe: Box<dyn Fn() -> String>,
    }
    
    impl Comparable {
        fn new<T: Ord + fmt::Debug + 'static>(a: T, b: T) -> Self {
            let a2 = format!("{:?}", a);
            let b2 = format!("{:?}", b);
            Comparable {
                compare_fn: Box::new(move || a.cmp(&b)),
                describe: Box::new(move || format!("{} vs {}", a2, b2)),
            }
        }
    
        fn result(&self) -> &'static str {
            match (self.compare_fn)() {
                std::cmp::Ordering::Less => "less",
                std::cmp::Ordering::Equal => "equal",
                std::cmp::Ordering::Greater => "greater",
            }
        }
    }
    
    // Multi-trait existential using a custom trait
    trait Printable: fmt::Display + fmt::Debug {}
    impl<T: fmt::Display + fmt::Debug> Printable for T {}
    
    fn print_all(items: &[Box<dyn Printable>]) -> Vec<String> {
        items.iter().map(|x| format!("{}", x)).collect()
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_box_dyn_display() {
            let items = make_showables();
            assert_eq!(format!("{}", items[0]), "42");
            assert_eq!(format!("{}", items[1]), "hello");
        }
    
        #[test]
        fn test_custom_showable() {
            let s = Showable::new(42, |x| x.to_string());
            assert_eq!(s.show(), "42");
            let s2 = Showable::new(String::from("world"), |x| x.clone());
            assert_eq!(s2.show(), "world");
        }
    
        #[test]
        fn test_comparable() {
            assert_eq!(Comparable::new(1, 2).result(), "less");
            assert_eq!(Comparable::new(5, 5).result(), "equal");
            assert_eq!(Comparable::new("z", "a").result(), "greater");
        }
    
        #[test]
        fn test_multi_trait() {
            let items: Vec<Box<dyn Printable>> = vec![Box::new(42), Box::new("hi")];
            let strs = print_all(&items);
            assert_eq!(strs, vec!["42", "hi"]);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_box_dyn_display() {
            let items = make_showables();
            assert_eq!(format!("{}", items[0]), "42");
            assert_eq!(format!("{}", items[1]), "hello");
        }
    
        #[test]
        fn test_custom_showable() {
            let s = Showable::new(42, |x| x.to_string());
            assert_eq!(s.show(), "42");
            let s2 = Showable::new(String::from("world"), |x| x.clone());
            assert_eq!(s2.show(), "world");
        }
    
        #[test]
        fn test_comparable() {
            assert_eq!(Comparable::new(1, 2).result(), "less");
            assert_eq!(Comparable::new(5, 5).result(), "equal");
            assert_eq!(Comparable::new("z", "a").result(), "greater");
        }
    
        #[test]
        fn test_multi_trait() {
            let items: Vec<Box<dyn Printable>> = vec![Box::new(42), Box::new("hi")];
            let strs = print_all(&items);
            assert_eq!(strs, vec!["42", "hi"]);
        }
    }

    Deep Comparison

    Comparison: Example 182 — Existential Types

    Basic Existential

    OCaml

    type showable = Show : 'a * ('a -> string) -> showable
    
    let show (Show (x, f)) = f x
    
    let items = [Show (42, string_of_int); Show ("hello", Fun.id)]
    let results = List.map show items
    

    Rust

    let items: Vec<Box<dyn fmt::Display>> = vec![Box::new(42), Box::new("hello")];
    let results: Vec<String> = items.iter().map(|x| format!("{}", x)).collect();
    

    Closure-Based Existential

    OCaml

    type showable = Show : 'a * ('a -> string) -> showable
    

    Rust

    struct Showable {
        show_fn: Box<dyn Fn() -> String>,
    }
    
    impl Showable {
        fn new<T: 'static>(value: T, to_string: fn(&T) -> String) -> Self {
            Showable { show_fn: Box::new(move || to_string(&value)) }
        }
    }
    

    First-Class Module vs Super-Trait

    OCaml

    module type PRINTABLE = sig
      type t
      val value : t
      val to_string : t -> string
    end
    
    let print_it (m : (module PRINTABLE)) =
      let module M = (val m) in M.to_string M.value
    

    Rust

    trait Printable: fmt::Display + fmt::Debug {}
    impl<T: fmt::Display + fmt::Debug> Printable for T {}
    
    fn print_all(items: &[Box<dyn Printable>]) -> Vec<String> {
        items.iter().map(|x| format!("{}", x)).collect()
    }
    

    Exercises

  • Create Vec<Box<dyn std::error::Error>> and store different error types, then iterate and print each.
  • Implement a Callback type using the closure existential: struct Callback { call: Box<dyn Fn(Event) -> ()> }.
  • Write a plugin registry that stores Vec<Box<dyn Plugin>> and calls plugin.name() and plugin.run() on each.
  • Open Source Repos