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
Box<dyn Display> and custom traitsCode 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
(value, function); Rust's closure-based approach bundles them in the captured environment — equivalent but differently structured.Box<dyn Trait> uses a vtable; the closure approach creates an independent dispatch mechanism — useful when the "trait" is just one function.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.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
Vec<Box<dyn std::error::Error>> and store different error types, then iterate and print each.Callback type using the closure existential: struct Callback { call: Box<dyn Fn(Event) -> ()> }.Vec<Box<dyn Plugin>> and calls plugin.name() and plugin.run() on each.