
The sky is the limit!
Today, I'm writing about what types can be used for other than checking code properties. It will involve a good chunk of dynamic typing, and yes it's in Rust. There are some wild ideas in it, so fasten your seatbelt and get ready for a ride!
The article is divided into introduction, background, three sections containing the main content, and a conclusion. The three sections in the middle each cover their own idea with a separate motivation. What connects is the way runtime type evaluation is applied. In that aspect, they build on top of each other.
Types are a very abstract concept. What even are they? For me, the answer depends quite a bit on the programming language and the general context of the discussion.
When I wrote my very first lines of program code, in C++, a type was for me just the thing to define a variable. As I got more practice, with C++ and Java, types in my mind became essentially equivalent to classes or primitives. But I didn’t think much about types anyway. They were just a necessity to make the compiler happy.
Expanding to JavaScript, I realized that types can also be hidden in the background. In that case, they must be right to make the runtime happy, which seemed to be more forgiving than the compiler. On the other hand, I hated it when errors only appeared at runtime that I knew a compiler could tell me before.
Then, I learned Haskell. Types became a completely different concept. It seemed like entire programs could be written in the type system itself. I was impressed.
After all of that, I learned Rust. I loved how strongly typed everything felt with Rust. Comparing to C and C++, Rust removed the most frustrating parts from them. Forgetting to initialize variabels was no longer possible, null pointers ceased to exist, and memory management became a blast.
Fast-forward to today. Rust showed me several completely new concepts that can be achieved with its clever type system. Lifetimes incorporate the memory management aspect inside the type. The distinction between &mut and & types defines if aliasing is allowed. And in a way, types implementing the Future trait describe an entire finite state machine.
But today I want to talk about runtime type evaluation in Rust. I’ve come across some practical programming problems that I wasn’t able to solve without some (safe) downcasts here and there. I then took it to extreme levels of dynamic typing that I didn’t expect to be possible. Along the way, I had to reconsider once again what a type actually is. And since I found the results quite interesting and surprising, I wanted to share it in this article.
In some languages, the type of every (non-primitive) value is embedded in machine code. It’s like a hidden field implicitly present in every object. This is one way to enable dynamic typing. But Rust does not include type information overhead with every value.
However, Rust offers ways to manually store type information which can be used also at runtime. It’s possible to transform a value of statically known type into a fat pointer that combines the value with a virtual function table (vtable) for one trait. These fat pointers are called trait objects.
Trait objects essentially provide opt-in runtime type information. But their power is fairly limited as they only gives access to the functions of a specific trait and its parents traits. To know if we are dealing with a specific type, one more trick is required.
Using only tools from the core standard library, we can ask the compiler for a TypeId of any type and store this for our own use at runtime. The compiler will then put a unique constant number there for the type ID.
Here is how type IDs are created. (Run it on the playground!%0A%7D%0A))
use core::any::{Any, TypeId};
fn main() {
let one_hundred = 100u32;
// Get the type ID usaing a value of that type.
let t0 = one_hundred.type_id();
// Get the type ID directly
let t1 = TypeId::of::<u32>();
assert_eq!(t0, t1)
}