๐Ÿฆ€ Functional Rust

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

Key Differences

ConceptOCamlRust
Type witnessGADT: `type _ ty = TInt : int tyTStr : string ty``TypeId` via `Any` trait
Type-safe retrievalPattern match on GADT witness refines type`downcast_ref::<T>()` โ€” returns `Option<&T>`
Open vs closedGADT is closed (fixed constructors)`Box<dyn Any>` is open (any `'static` type)
Combined capabilitiesFirst-class module with witness + operationsCustom supertrait: `trait AnyDisplay: Any + Display`
Enum alternativePolymorphic 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>();  // None

Display + 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 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

๐Ÿช Show OCaml equivalent
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 }
 }
}