182: Existential Types
Difficulty: 4 Level: Advanced Store values of any type alongside their operations โ the type is forgotten, the capability is preserved.The Problem This Solves
You have values of different types โ `i32`, `String`, `f64`, a custom struct โ and you want to put them all in one collection. Not as raw bytes; you want to be able to do something with each value, like display it. But you don't want to enumerate all possible types in an enum. An existential type says: "there exists some type T, and I have a value of that T, and T supports these operations." The concrete T is hidden from the outside โ only the operations are visible. This is the key idea behind `Box<dyn Trait>`. The contrast with generics: `fn show<T: Display>(v: T)` requires the caller to name `T`. An existential says the callee picks `T` โ and nobody needs to name it.The Intuition
When you write `Box::new(42_i32) as Box<dyn Display>`, you're packing two things into the heap: the value `42` and a pointer to `i32`'s implementation of `Display`. The type `i32` is erased โ you can only call `Display`'s methods. This is an existential: some type T implements `Display`, and here's a value of that T. OCaml achieves the same with GADT constructors: `Show : 'a * ('a -> string) -> showable` packs a value with its display function, hiding `'a`. Rust's `Box<dyn Trait>` is the runtime analog with vtable dispatch.How It Works in Rust
use std::fmt;
// Approach 1: Box<dyn Trait> โ Rust's native existential
fn make_showables() -> Vec<Box<dyn fmt::Display>> {
vec![
Box::new(42), // i32 erased โ only Display survives
Box::new("hello"), // &str erased
Box::new(3.14), // f64 erased
]
}
for item in make_showables() {
println!("{}", item); // dispatch via vtable โ correct impl called
}
Approach 2: closure-based packing (mirrors OCaml GADT constructor):
struct Showable {
show_fn: Box<dyn Fn() -> String>,
}
impl Showable {
fn new<T: 'static>(value: T, to_string: fn(&T) -> String) -> Self {
// Both `value` and `to_string` are captured โ T is erased from the outside
Showable {
show_fn: Box::new(move || to_string(&value)),
}
}
fn show(&self) -> String { (self.show_fn)() }
}
let items = vec![
Showable::new(42, |x| x.to_string()),
Showable::new(String::from("hello"), |x| x.clone()),
Showable::new(3.14f64, |x| format!("{}", x)),
];
Multi-trait existential via supertrait:
trait Printable: fmt::Display + fmt::Debug {}
impl<T: fmt::Display + fmt::Debug> Printable for T {}
let items: Vec<Box<dyn Printable>> = vec![Box::new(42), Box::new("hi")];
// Can call both display and debug on each item
What This Unlocks
- Heterogeneous collections โ `Vec<Box<dyn Event>>` for event queues with different event types
- Plugin interfaces โ store plugins as `Box<dyn Plugin>` regardless of their concrete types
- Capability bundles โ pack a value with its serializer, validator, or formatter
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Existential encoding | GADT constructor: `Show : 'a * ('a -> string) -> showable` | `Box<dyn Trait>` via vtable |
| Closure packing | `let pack v f = Show (v, f)` | `struct { show_fn: Box<dyn Fn() -> String> }` |
| Multi-capability | First-class module with multiple fields | Super-trait combining multiple traits |
| Recovering T | Pattern match on GADT constructor | Not possible โ use `Any::downcast_ref` if needed |
| Allocation | GC-managed heap | `Box` โ heap allocation, drop via `Drop` |
// 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()
}
fn main() {
// Approach 1
let items = make_showables();
for item in &items {
println!("{}", item);
}
// Approach 2
let showables = vec![
Showable::new(42, |x| x.to_string()),
Showable::new(String::from("hello"), |x| x.clone()),
Showable::new(3.14f64, |x| x.to_string()),
];
for s in &showables {
println!("{}", s.show());
}
// Approach 3
let comparisons = vec![
Comparable::new(1, 2),
Comparable::new(5, 5),
Comparable::new("z", "a"),
];
for c in &comparisons {
println!("{}: {}", (c.describe)(), c.result());
}
println!("โ All examples running");
}
#[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"]);
}
}
(* Example 182: Existential Types *)
(* Hide the concrete type while retaining ability to use it *)
(* Approach 1: Existential via GADT *)
type showable = Show : 'a * ('a -> string) -> showable
let show (Show (x, f)) = f x
let show_list : showable list = [
Show (42, string_of_int);
Show ("hello", Fun.id);
Show (3.14, string_of_float);
Show (true, string_of_bool);
]
(* Approach 2: First-class module existential *)
module type PRINTABLE = sig
type t
val value : t
val to_string : t -> string
end
let make_printable (type a) (to_s : a -> string) (v : a) : (module PRINTABLE) =
(module struct
type t = a
let value = v
let to_string = to_s
end)
let print_it (m : (module PRINTABLE)) =
let module M = (val m) in
M.to_string M.value
(* Approach 3: Existential with comparison *)
type comparable = Cmp : 'a * 'a * ('a -> 'a -> int) -> comparable
let compare_pair (Cmp (a, b, cmp)) =
let r = cmp a b in
if r < 0 then "less" else if r = 0 then "equal" else "greater"
let () =
(* Test Approach 1 *)
let results = List.map show show_list in
assert (List.nth results 0 = "42");
assert (List.nth results 1 = "hello");
assert (List.nth results 3 = "true");
(* Test Approach 2 *)
let items = [
make_printable string_of_int 42;
make_printable Fun.id "world";
] in
assert (print_it (List.nth items 0) = "42");
assert (print_it (List.nth items 1) = "world");
(* Test Approach 3 *)
assert (compare_pair (Cmp (1, 2, compare)) = "less");
assert (compare_pair (Cmp (5, 5, compare)) = "equal");
assert (compare_pair (Cmp ("z", "a", String.compare)) = "greater");
print_endline "โ All tests passed"
๐ Detailed Comparison
Comparison: Example 182 โ Existential Types
Basic Existential
OCaml
๐ช Show OCaml equivalent
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 itemsRust
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
๐ช Show OCaml equivalent
type showable = Show : 'a * ('a -> string) -> showableRust
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
๐ช Show OCaml equivalent
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.valueRust
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()
}