ExamplesBy LevelBy TopicLearning Paths
183 Expert

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

  • • Use Box<dyn Any> for storing heterogeneous values with type erasure
  • • Learn downcast_ref::<T>() for safe type recovery from Any
  • • Implement a HeteroVec with typed push and typed get operations
  • • Understand the 'static bound on Any and why non-'static types cannot be stored
  • Code 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>();  // None

    Key Differences

  • Safety: Rust's 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.
  • Typed maps: Rust's TypeMap pattern uses TypeId as keys for typed storage of one value per type; OCaml uses module types or phantom-typed keys.
  • Performance: 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

  • Build a TypeMap that stores at most one value of each type: insert<T: Any>(val: T) and get<T: Any>() -> Option<&T>.
  • Implement a safe downcasting iterator: given Vec<Box<dyn Any>>, collect all values of a specific type T.
  • Write a property bag for UI components: PropertyBag::set("color", "#ff0000") and PropertyBag::get::<String>("color").
  • Open Source Repos