ExamplesBy LevelBy TopicLearning Paths
385 Intermediate

385: Trait Objects and `Any` (Runtime Type Information)

Functional Programming

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

  • • Understand how std::any::Any provides runtime type information in a statically typed language
  • • Learn how TypeId::of::<T>() creates a unique type identity usable as a map key
  • • See how downcast_ref::<T>() safely recovers the concrete type from dyn Any
  • • Understand the 'static bound requirement on Any and why lifetime-bearing types cannot implement it
  • • Learn the type-safe heterogeneous map pattern using HashMap<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 safety: Rust's downcast_ref returns Option<&T> — explicit failure handling; OCaml's Obj.magic is unsafe and unchecked.
  • Type identity: Rust uses TypeId (opaque hash of type); OCaml uses GADT witnesses or Hashtbl with type-indexed keys from libraries.
  • Lifetime restriction: Rust's Any requires T: 'static; OCaml has no equivalent restriction since the GC manages all lifetimes.
  • Ergonomics: Rust requires explicit 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()));
        }
    }
    ✓ Tests Rust test suite
    #[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

  • Event bus: Build a type-safe event bus using 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.
  • Typed arena: Create an arena allocator that stores values of any type and retrieves them by a typed handle. The handle encodes the type at compile time, so get::<String>(handle) only compiles if the handle was created with a String.
  • Dynamic dispatch benchmark: Compare three approaches for a heterogeneous collection: 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.
  • Open Source Repos