385: Trait Objects and `Any` (Runtime Type Information)
Tutorial Video
Text description (accessibility)
This video demonstrates the "385: Trait Objects and `Any` (Runtime Type Information)" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. Statically typed languages normally cannot store values of unknown type and later recover their original type. Key difference from OCaml: 1. **Downcast safety**: Rust's `downcast_ref` returns `Option<&T>` — explicit failure handling; OCaml's `Obj.magic` is unsafe and unchecked.
Tutorial
The Problem
Statically typed languages normally cannot store values of unknown type and later recover their original type. Rust's std::any::Any trait provides controlled runtime type information: any 'static type automatically implements Any, and dyn Any enables downcasting back to the original type with downcast_ref::<T>(). This enables heterogeneous containers, scripting engine value types, dependency injection containers, and event systems where the payload type varies.
Any and TypeId appear in the anymap crate, Bevy's ECS component storage, actix's actor message system, and any system requiring type-erased storage with safe recovery.
🎯 Learning Outcomes
std::any::Any provides runtime type information in a statically typed languageTypeId::of::<T>() creates a unique type identity usable as a map keydowncast_ref::<T>() safely recovers the concrete type from dyn Any'static bound requirement on Any and why lifetime-bearing types cannot implement itHashMap<TypeId, Box<dyn Any>>Code Example
#![allow(clippy::all)]
//! Any Trait for Runtime Type Information
use std::any::{Any, TypeId};
use std::collections::HashMap;
/// Describe a value using downcasting
pub fn describe(val: &dyn Any) -> String {
if let Some(n) = val.downcast_ref::<i32>() {
format!("i32: {}", n)
} else if let Some(s) = val.downcast_ref::<String>() {
format!("String: {}", s)
} else if let Some(b) = val.downcast_ref::<bool>() {
format!("bool: {}", b)
} else {
format!("Unknown type: {:?}", val.type_id())
}
}
/// Type-safe heterogeneous map
pub struct TypeMap {
map: HashMap<TypeId, Box<dyn Any>>,
}
impl TypeMap {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn insert<T: Any>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Any>(&self) -> Option<&T> {
self.map.get(&TypeId::of::<T>())?.downcast_ref::<T>()
}
}
impl Default for TypeMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_i32() {
assert_eq!(describe(&42i32), "i32: 42");
}
#[test]
fn test_describe_string() {
assert_eq!(describe(&String::from("hi")), "String: hi");
}
#[test]
fn test_describe_bool() {
assert_eq!(describe(&true), "bool: true");
}
#[test]
fn test_typemap() {
let mut m = TypeMap::new();
m.insert(42i32);
m.insert("hello".to_string());
assert_eq!(m.get::<i32>(), Some(&42));
assert_eq!(m.get::<String>(), Some(&"hello".to_string()));
}
}Key Differences
downcast_ref returns Option<&T> — explicit failure handling; OCaml's Obj.magic is unsafe and unchecked.TypeId (opaque hash of type); OCaml uses GADT witnesses or Hashtbl with type-indexed keys from libraries.Any requires T: 'static; OCaml has no equivalent restriction since the GC manages all lifetimes.downcast_ref::<T>() at every use site; OCaml's GADT approach can sometimes infer the type from context.OCaml Approach
OCaml handles heterogeneous storage through existential types (first-class modules with type t) or the Obj module for unsafe runtime values. A type-safe heterogeneous map in OCaml uses GADT witnesses: type 'a key = Key : TypeId * ('a -> int) -> 'a key. The hmap library provides this. Unlike Rust's explicit TypeId, OCaml hides type representations behind the GC.
Full Source
#![allow(clippy::all)]
//! Any Trait for Runtime Type Information
use std::any::{Any, TypeId};
use std::collections::HashMap;
/// Describe a value using downcasting
pub fn describe(val: &dyn Any) -> String {
if let Some(n) = val.downcast_ref::<i32>() {
format!("i32: {}", n)
} else if let Some(s) = val.downcast_ref::<String>() {
format!("String: {}", s)
} else if let Some(b) = val.downcast_ref::<bool>() {
format!("bool: {}", b)
} else {
format!("Unknown type: {:?}", val.type_id())
}
}
/// Type-safe heterogeneous map
pub struct TypeMap {
map: HashMap<TypeId, Box<dyn Any>>,
}
impl TypeMap {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn insert<T: Any>(&mut self, value: T) {
self.map.insert(TypeId::of::<T>(), Box::new(value));
}
pub fn get<T: Any>(&self) -> Option<&T> {
self.map.get(&TypeId::of::<T>())?.downcast_ref::<T>()
}
}
impl Default for TypeMap {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_i32() {
assert_eq!(describe(&42i32), "i32: 42");
}
#[test]
fn test_describe_string() {
assert_eq!(describe(&String::from("hi")), "String: hi");
}
#[test]
fn test_describe_bool() {
assert_eq!(describe(&true), "bool: true");
}
#[test]
fn test_typemap() {
let mut m = TypeMap::new();
m.insert(42i32);
m.insert("hello".to_string());
assert_eq!(m.get::<i32>(), Some(&42));
assert_eq!(m.get::<String>(), Some(&"hello".to_string()));
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_describe_i32() {
assert_eq!(describe(&42i32), "i32: 42");
}
#[test]
fn test_describe_string() {
assert_eq!(describe(&String::from("hi")), "String: hi");
}
#[test]
fn test_describe_bool() {
assert_eq!(describe(&true), "bool: true");
}
#[test]
fn test_typemap() {
let mut m = TypeMap::new();
m.insert(42i32);
m.insert("hello".to_string());
assert_eq!(m.get::<i32>(), Some(&42));
assert_eq!(m.get::<String>(), Some(&"hello".to_string()));
}
}
Deep Comparison
OCaml vs Rust: 385-trait-objects-any
Exercises
TypeId keys and Vec<Box<dyn Any>> values. Implement publish<E: Any>(event: E) and subscribe<E: Any, F: Fn(&E)>(handler: F), dispatching stored events to matching handlers.get::<String>(handle) only compiles if the handle was created with a String.Vec<Box<dyn Any>> with downcasting, Vec<Box<dyn Trait>> with vtables, and an enum-based closed set. Measure access time for 1 million elements.