183: Heterogeneous Vec with Safe Downcast
Difficulty: 4 Level: Advanced Store values of different types in one `Vec` and retrieve them by their original type โ safely, without crashes.The Problem This Solves
Sometimes you need a collection that holds values of genuinely different types: a `Vec` that contains both `i64` and `String` and `bool`. An enum works when the set of types is fixed and known at compile time. But what if you want an open-ended collection that accepts any type at insertion time and returns it at retrieval time? The `Any` trait provides runtime type identity. You can store `Box<dyn Any>` and later ask "is this an `i64`?" via `downcast_ref::<i64>()`. It returns `None` on type mismatch โ safe, no panics, no undefined behavior. This is the Rust equivalent of OCaml's GADT type witnesses: a mechanism for type-safe dynamic dispatch where the concrete type is recovered at runtime rather than enforced at compile time.The Intuition
Every Rust type has a `TypeId` โ a unique identifier. The `Any` trait enables two operations: get the `TypeId` of a value, and attempt a downcast to a concrete type. `downcast_ref::<T>()` checks the stored `TypeId` against `TypeId::of::<T>()` and returns `Some(&T)` if they match. This is different from type erasure: with `Box<dyn Trait>`, you can call trait methods but can't recover the type. With `Box<dyn Any>`, you don't have trait methods, but you can recover the type. Combine both via a custom `AnyDisplay` supertrait if you want both display capabilities and downcasting.How It Works in Rust
use std::any::Any;
struct HeteroVec {
items: Vec<Box<dyn Any>>,
}
impl HeteroVec {
fn push<T: 'static>(&mut self, val: T) {
self.items.push(Box::new(val)); // type is erased to Box<dyn Any>
}
fn get<T: 'static>(&self, index: usize) -> Option<&T> {
// downcast_ref checks the TypeId and returns Some if it matches
self.items.get(index)?.downcast_ref::<T>()
}
}
let mut hv = HeteroVec::new();
hv.push(42i64);
hv.push(String::from("hello"));
hv.push(true);
hv.get::<i64>(0) // Some(&42)
hv.get::<i64>(1) // None โ position 1 holds a String, not i64
hv.get::<String>(1) // Some(&"hello")
Combine `Any` with `Display` for both downcasting and formatted output:
trait AnyDisplay: Any + std::fmt::Display {
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + std::fmt::Display> AnyDisplay for T {
fn as_any(&self) -> &dyn Any { self }
}
struct DisplayVec {
items: Vec<Box<dyn AnyDisplay>>,
}
impl DisplayVec {
fn display_all(&self) -> Vec<String> {
self.items.iter().map(|x| format!("{}", x)).collect()
}
fn get<T: 'static>(&self, i: usize) -> Option<&T> {
self.items.get(i)?.as_any().downcast_ref::<T>()
}
}
When the type set is closed, prefer enum โ it's cleaner and faster:
#[derive(Debug)]
enum Value { Int(i64), Str(String), Bool(bool), Float(f64) }
// Exhaustive pattern matching at extraction โ no runtime TypeId lookup
let vals = vec![Value::Int(1), Value::Str("x".into()), Value::Bool(false)];
What This Unlocks
- Dynamic registries โ store handlers/plugins of arbitrary types, retrieve by type key
- Scripting interop โ bridge between a typed Rust core and a dynamic scripting layer
- Test harnesses โ collect test results of different types in a single runner output
Key Differences
| Concept | OCaml | Rust | |
|---|---|---|---|
| Type witness | GADT: `type _ ty = TInt : int ty | TStr : string ty` | `TypeId` via `Any` trait |
| Type-safe retrieval | Pattern match on GADT witness refines type | `downcast_ref::<T>()` โ returns `Option<&T>` | |
| Open vs closed | GADT is closed (fixed constructors) | `Box<dyn Any>` is open (any `'static` type) | |
| Combined capabilities | First-class module with witness + operations | Custom supertrait: `trait AnyDisplay: Any + Display` | |
| Enum alternative | Polymorphic variant / ADT | `enum Value { Int(i64), Str(String), ... }` |
// Example 183: Heterogeneous Vector with Safe Downcast
// Store different types in one Vec, downcast safely via Any
use std::any::Any;
use std::fmt;
// === Approach 1: Box<dyn Any> with downcast ===
struct HeteroVec {
items: Vec<Box<dyn Any>>,
}
impl HeteroVec {
fn new() -> Self { HeteroVec { items: Vec::new() } }
fn push<T: 'static>(&mut self, val: T) {
self.items.push(Box::new(val));
}
fn get<T: 'static>(&self, index: usize) -> Option<&T> {
self.items.get(index)?.downcast_ref::<T>()
}
fn len(&self) -> usize { self.items.len() }
}
// === Approach 2: Custom trait object with Display + Any ===
trait AnyDisplay: fmt::Display {
fn as_any(&self) -> &dyn Any;
}
impl<T: 'static + fmt::Display> AnyDisplay for T {
fn as_any(&self) -> &dyn Any { self }
}
struct DisplayVec {
items: Vec<(Box<dyn Any>, Box<dyn fmt::Display>)>,
}
impl DisplayVec {
fn new() -> Self { DisplayVec { items: Vec::new() } }
fn push<T: 'static + fmt::Display + Clone>(&mut self, val: T) {
self.items.push((Box::new(val.clone()), Box::new(val)));
}
fn get<T: 'static>(&self, index: usize) -> Option<&T> {
self.items.get(index)?.0.downcast_ref::<T>()
}
fn display_all(&self) -> Vec<String> {
self.items.iter().map(|(_, d)| format!("{}", d)).collect()
}
}
// === Approach 3: Enum-based (like OCaml value type) ===
#[derive(Debug, Clone)]
enum Value {
Int(i64),
Str(String),
Bool(bool),
Float(f64),
}
impl Value {
fn as_int(&self) -> Option<i64> {
match self { Value::Int(n) => Some(*n), _ => None }
}
fn as_str(&self) -> Option<&str> {
match self { Value::Str(s) => Some(s), _ => None }
}
fn as_bool(&self) -> Option<bool> {
match self { Value::Bool(b) => Some(*b), _ => None }
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Value::Int(n) => write!(f, "{}", n),
Value::Str(s) => write!(f, "{}", s),
Value::Bool(b) => write!(f, "{}", b),
Value::Float(x) => write!(f, "{}", x),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hetero_vec() {
let mut hv = HeteroVec::new();
hv.push(42i64);
hv.push(String::from("hello"));
hv.push(true);
assert_eq!(hv.get::<i64>(0), Some(&42));
assert_eq!(hv.get::<i64>(1), None);
assert_eq!(hv.get::<String>(1), Some(&String::from("hello")));
assert_eq!(hv.get::<bool>(2), Some(&true));
assert_eq!(hv.len(), 3);
}
#[test]
fn test_display_vec() {
let mut dv = DisplayVec::new();
dv.push(42i64);
dv.push(String::from("hi"));
assert_eq!(dv.display_all(), vec!["42", "hi"]);
assert_eq!(dv.get::<i64>(0), Some(&42));
}
#[test]
fn test_value_enum() {
assert_eq!(Value::Int(42).as_int(), Some(42));
assert_eq!(Value::Int(42).as_str(), None);
assert_eq!(Value::Str("x".into()).as_str(), Some("x"));
assert_eq!(Value::Bool(true).as_bool(), Some(true));
}
#[test]
fn test_value_display() {
assert_eq!(format!("{}", Value::Int(42)), "42");
assert_eq!(format!("{}", Value::Str("hi".into())), "hi");
assert_eq!(format!("{}", Value::Float(3.14)), "3.14");
}
}
(* Example 183: Heterogeneous Vector with Safe Downcast *)
(* Approach 1: GADT-based heterogeneous list *)
type _ ty =
| TInt : int ty
| TString : string ty
| TBool : bool ty
| TFloat : float ty
type entry = Entry : 'a ty * 'a -> entry
let get_int (Entry (ty, v)) : int option =
match ty with TInt -> Some v | _ -> None
let get_string (Entry (ty, v)) : string option =
match ty with TString -> Some v | _ -> None
let get_bool (Entry (ty, v)) : bool option =
match ty with TBool -> Some v | _ -> None
let to_string_entry (Entry (ty, v)) =
match ty with
| TInt -> string_of_int v
| TString -> v
| TBool -> string_of_bool v
| TFloat -> string_of_float v
(* Approach 2: Using polymorphic variants + objects *)
type value =
| VInt of int
| VStr of string
| VBool of bool
| VFloat of float
let value_to_string = function
| VInt n -> string_of_int n
| VStr s -> s
| VBool b -> string_of_bool b
| VFloat f -> string_of_float f
let as_int = function VInt n -> Some n | _ -> None
let as_string = function VStr s -> Some s | _ -> None
(* Approach 3: Dynamic type with type witness *)
type (_, _) eq = Refl : ('a, 'a) eq
module type DYNAMIC = sig
type t
val inject : 'a ty -> 'a -> t
val project : 'a ty -> t -> 'a option
end
module Dynamic : DYNAMIC = struct
type t = entry
let inject ty v = Entry (ty, v)
let project : type a. a ty -> t -> a option = fun ty (Entry (ty', v)) ->
match ty, ty' with
| TInt, TInt -> Some v
| TString, TString -> Some v
| TBool, TBool -> Some v
| TFloat, TFloat -> Some v
| _ -> None
end
let () =
(* Test Approach 1 *)
let entries = [
Entry (TInt, 42);
Entry (TString, "hello");
Entry (TBool, true);
Entry (TFloat, 3.14);
] in
assert (get_int (List.nth entries 0) = Some 42);
assert (get_int (List.nth entries 1) = None);
assert (get_string (List.nth entries 1) = Some "hello");
let strs = List.map to_string_entry entries in
assert (List.nth strs 0 = "42");
assert (List.nth strs 1 = "hello");
(* Test Approach 2 *)
let vals = [VInt 1; VStr "x"; VBool false] in
assert (as_int (List.nth vals 0) = Some 1);
assert (as_int (List.nth vals 1) = None);
assert (as_string (List.nth vals 1) = Some "x");
(* Test Approach 3 *)
let d = Dynamic.inject TInt 99 in
assert (Dynamic.project TInt d = Some 99);
assert (Dynamic.project TString d = None);
print_endline "โ All tests passed"
๐ Detailed Comparison
Comparison: Example 183 โ Heterogeneous Vector
GADT Type Witness vs Any
OCaml
๐ช Show OCaml equivalent
type _ ty = TInt : int ty | TString : string ty | TBool : bool ty
type entry = Entry : 'a ty * 'a -> entry
let get_int (Entry (ty, v)) = match ty with TInt -> Some v | _ -> None
let entries = [Entry (TInt, 42); Entry (TString, "hello")]Rust
use std::any::Any;
let mut items: Vec<Box<dyn Any>> = Vec::new();
items.push(Box::new(42i64));
items.push(Box::new(String::from("hello")));
let n: Option<&i64> = items[0].downcast_ref::<i64>(); // Some(&42)
let s: Option<&i64> = items[1].downcast_ref::<i64>(); // NoneDisplay + Downcast
OCaml
๐ช Show OCaml equivalent
let to_string_entry (Entry (ty, v)) = match ty with
| TInt -> string_of_int v
| TString -> v
| TBool -> string_of_bool vRust
trait AnyDisplay: Any + fmt::Display {
fn as_any(&self) -> &dyn Any;
}
impl<T: Any + fmt::Display> AnyDisplay for T {
fn as_any(&self) -> &dyn Any { self }
}
// Can display AND downcast
let item: Box<dyn AnyDisplay> = Box::new(42);
println!("{}", item); // Display
let n = item.as_any().downcast_ref::<i32>(); // DowncastEnum-Based
OCaml
๐ช Show OCaml equivalent
type value = VInt of int | VStr of string | VBool of bool
let as_int = function VInt n -> Some n | _ -> NoneRust
enum Value { Int(i64), Str(String), Bool(bool) }
impl Value {
fn as_int(&self) -> Option<i64> {
match self { Value::Int(n) => Some(*n), _ => None }
}
}