Heterogeneous Vector with Safe Downcast
Functional Programming
Tutorial
The Problem
Sometimes you need a single collection to hold values of different types — a property bag, an event queue with mixed event types, a dynamic configuration store. Box<dyn Any> erases type information completely, but downcast_ref::<T>() recovers it safely: the downcast checks the stored TypeId against the requested type and returns Option<&T>. This is the runtime equivalent of type-safe storage — safer than transmute but with runtime dispatch.
🎯 Learning Outcomes
Box<dyn Any> for storing heterogeneous values with type erasuredowncast_ref::<T>() for safe type recovery from AnyHeteroVec with typed push and typed get operations'static bound on Any and why non-'static types cannot be storedCode Example
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>(); // NoneKey Differences
downcast_ref checks TypeId at runtime — safe by construction; OCaml's Obj.magic has no check — inherently unsafe.'static bound**: Rust's Any requires 'static to ensure stored values outlive any reference they might contain; OCaml has no lifetime concept.TypeMap pattern uses TypeId as keys for typed storage of one value per type; OCaml uses module types or phantom-typed keys.downcast_ref is O(1) — one TypeId comparison; no allocation, no iteration.OCaml Approach
OCaml's Obj.magic is the unsafe equivalent — it casts any value to any type without checking. Safe alternatives use GADTs:
type any = Any : 'a * ('a -> string) -> any
Or use polymorphic functions at the call site. OCaml's GC-managed values are all pointer-sized, making type erasure and "untyped" storage easy but unsafe without GADT wrappers.
Full Source
#![allow(clippy::all)]
// 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");
}
}
✓ Tests
Rust test suite
#[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");
}
}
Deep Comparison
Comparison: Example 183 — Heterogeneous Vector
GADT Type Witness vs Any
OCaml
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>(); // None
Display + Downcast
OCaml
let to_string_entry (Entry (ty, v)) = match ty with
| TInt -> string_of_int v
| TString -> v
| TBool -> string_of_bool v
Rust
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>(); // Downcast
Enum-Based
OCaml
type value = VInt of int | VStr of string | VBool of bool
let as_int = function VInt n -> Some n | _ -> None
Rust
enum Value { Int(i64), Str(String), Bool(bool) }
impl Value {
fn as_int(&self) -> Option<i64> {
match self { Value::Int(n) => Some(*n), _ => None }
}
}
Exercises
TypeMap that stores at most one value of each type: insert<T: Any>(val: T) and get<T: Any>() -> Option<&T>.Vec<Box<dyn Any>>, collect all values of a specific type T.PropertyBag::set("color", "#ff0000") and PropertyBag::get::<String>("color").