BBuzzard

PhantomData<T> is a zero-sized field that says "this type logically involves T" without storing a T. It lets you tag a type with a marker: Id<User> and Id<Team> are different types even though both just hold a u64. The same trick builds "typestate" APIs, where invalid method calls are compile errors.

Every generic parameter is Sized by default: its size is known at compile time. ?Sized opts out so the parameter can also be str, [T], or dyn Trait, which must then live behind a pointer (&T, Box<T>).

dyn Any is a type-erased value that can be downcast back with downcast_ref::<T>(). It is how panic payloads and some plugin systems carry arbitrary values. TypeId is the runtime identity Any uses, and type_name gives a readable name for debugging, but not a stable one.

Typed ids with PhantomData

struct Id<T> {
    n: u64,
    _ty: PhantomData<T>,
}

impl<T> Id<T> {
    fn new(n: u64) -> Self {
        Id { n, _ty: PhantomData }
    }
}

struct User;
struct Team;

fn load_user(id: Id<User>) -> String {
    format!("user #{}", id.n)
}

fn demo_typed_ids() {
    println!("-- typed ids --");

    let u = Id::<User>::new(1);
    let t = Id::<Team>::new(1);
    println!("{}", load_user(u));

    // load_user(t); // error[E0308]: expected `Id<User>`, found `Id<Team>`
    println!("team id {} size of Id<Team> {} bytes", t.n, size_of::<Id<Team>>());
}

Typestate: methods that only exist in certain states

struct Open;
struct Closed;

struct Door<State> {
    name: String,
    _state: PhantomData<State>,
}

impl Door<Closed> {
    fn new(name: &str) -> Self {
        Door { name: name.into(), _state: PhantomData }
    }

    fn open(self) -> Door<Open> {
        println!("  {} opens", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

impl Door<Open> {
    fn walk_through(&self) {
        println!("  walking through {}", self.name);
    }

    fn close(self) -> Door<Closed> {
        println!("  {} closes", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

fn demo_typestate() {
    println!("-- typestate --");

    let d = Door::new("front");

    // d.walk_through(); // error[E0599]: no method named `walk_through` found for `Door<Closed>`
    let d = d.open();
    d.walk_through();

    let _closed = d.close();
}

Units of measure with zero runtime cost

#[derive(Debug, Clone, Copy)]
struct Meters;
#[derive(Debug, Clone, Copy)]
struct Seconds;

#[derive(Debug, Clone, Copy)]
struct Quantity<U> {
    v: f64,
    _u: PhantomData<U>,
}

impl<U> std::ops::Add for Quantity<U> {
    type Output = Quantity<U>;
    fn add(self, o: Self) -> Self {
        Quantity { v: self.v + o.v, _u: PhantomData }
    }
}

fn demo_units() {
    println!("-- units --");

    let a: Quantity<Meters> = Quantity { v: 3.0, _u: PhantomData };
    let b: Quantity<Meters> = Quantity { v: 4.5, _u: PhantomData };
    let t: Quantity<Seconds> = Quantity { v: 2.0, _u: PhantomData };
    println!("a + b = {} m, t = {} s", (a + b).v, t.v);

    // a + t; // error[E0308]: mismatched types: expected `Quantity<Meters>`, found `Quantity<Seconds>`
}

Sized and ?Sized

fn name_len(s: &str) -> usize {
    s.len()
}

fn show<T: Display + ?Sized>(t: &T) -> String {
    format!("<{t}>")
}

fn show_sized<T: Display>(t: &T) -> String {
    format!("[{t}]")
}

fn demo_sized() {
    println!("-- Sized / ?Sized --");

    let boxed: Box<str> = "hello".into();
    println!("{} {}", name_len(&boxed), size_of_val(&*boxed));
    println!("{} {}", show("a str"), show(&5)); // T = str is allowed because of ?Sized
    println!("{}", show_sized(&"a &str")); // T = &str, which is Sized
    // show_sized::<str>("x"); // error[E0277]: the size for values of type `str` cannot be known
    let slice: &[i32] = &[1, 2, 3];
    let d: &dyn Debug = &slice;
    println!("unsized values behind pointers: {slice:?} {d:?}");
}

Any and downcasting

fn describe(value: &dyn Any) -> String {
    if let Some(n) = value.downcast_ref::<i32>() {
        format!("i32 {n}")
    } else if let Some(s) = value.downcast_ref::<String>() {
        format!("String {s:?}")
    } else if value.is::<f64>() {
        "some f64".into()
    } else {
        "unknown".into()
    }
}

fn demo_any() {
    println!("-- Any --");

    println!(
        "{} | {} | {} | {}",
        describe(&7i32),
        describe(&String::from("hi")),
        describe(&1.5f64),
        describe(&'c')
    );

    let items: Vec<Box<dyn Any>> =
        vec![Box::new(1i32), Box::new("str slice"), Box::new(2i32)];
    let ints: i32 = items.iter().filter_map(|b| b.downcast_ref::<i32>()).sum();
    println!("sum of the i32s {ints}");

    let b: Box<dyn Any> = Box::new(String::from("owned"));
    match b.downcast::<String>() {
        Ok(s) => println!("downcast Box to String {s}"),
        Err(_) => println!("not a String"),
    }
}

A type map keyed by TypeId

struct Resources {
    map: HashMap<TypeId, Box<dyn Any>>,
}

impl Resources {
    fn insert<T: Any>(&mut self, v: T) {
        self.map.insert(TypeId::of::<T>(), Box::new(v));
    }

    fn get<T: Any>(&self) -> Option<&T> {
        self.map.get(&TypeId::of::<T>())?.downcast_ref()
    }
}

#[derive(Debug)]
struct Gravity(f64);

fn demo_type_map() {
    println!("-- type map --");

    let mut r = Resources { map: HashMap::new() };
    r.insert(Gravity(9.81));
    r.insert(String::from("level-1"));
    println!("{:?} {:?} {:?}", r.get::<Gravity>(), r.get::<String>(), r.get::<u8>());
    println!("g = {}", r.get::<Gravity>().map_or(0.0, |g| g.0));
}

type_name and TypeId

fn demo_type_name() {
    println!("-- type_name / TypeId --");

    println!("{}", type_name::<Vec<i32>>());
    println!("{}", type_name::<Option<&str>>());

    let closure = |x: i32| x + 1;
    println!("of_val {} / {}", type_name_of_val(&1.0f32), type_name_of_val(&closure(1)));
    println!("TypeId equal for same type: {}", TypeId::of::<u8>() == TypeId::of::<u8>());
    println!("TypeId differs: {}", TypeId::of::<u8>() != TypeId::of::<i8>());
}
$ cargo run --bin 57_type_level
-- typed ids --
user #1
team id 1 size of Id<Team> 8 bytes
-- typestate --
  front opens
  walking through front
  front closes
-- units --
a + b = 7.5 m, t = 2 s
-- Sized / ?Sized --
5 5
<a str> <5>
[a &str]
unsized values behind pointers: [1, 2, 3] [1, 2, 3]
-- Any --
i32 7 | String "hi" | some f64 | unknown
sum of the i32s 3
downcast Box to String owned
-- type map --
Some(Gravity(9.81)) Some("level-1") None
g = 9.81
-- type_name / TypeId --
alloc::vec::Vec<i32>
core::option::Option<&str>
of_val f32 / i32
TypeId equal for same type: true
TypeId differs: true

Full program

57_type_level/main.rs · 238 lines
View on GitHub
// 57. Type-level tools: PhantomData, Sized, Any, and type_name
//
// PhantomData<T> is a zero-sized field that says "this type logically involves T"
// without storing a T. It lets you tag a type with a marker: Id<User> and Id<Team> are
// different types even though both just hold a u64. The same trick builds "typestate"
// APIs, where invalid method calls are compile errors.
//
// Every generic parameter is Sized by default: its size is known at compile time. ?Sized
// opts out so the parameter can also be str, [T], or dyn Trait, which must then live
// behind a pointer (&T, Box<T>).
//
// dyn Any is a type-erased value that can be downcast back with downcast_ref::<T>(). It
// is how panic payloads and some plugin systems carry arbitrary values. TypeId is the
// runtime identity Any uses, and type_name gives a readable name for debugging, but not
// a stable one.
//
// Run: cargo run --bin 57_type_level

use std::any::{Any, TypeId, type_name, type_name_of_val};
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;

struct Id<T> {
    n: u64,
    _ty: PhantomData<T>,
}

impl<T> Id<T> {
    fn new(n: u64) -> Self {
        Id { n, _ty: PhantomData }
    }
}

struct User;
struct Team;

fn load_user(id: Id<User>) -> String {
    format!("user #{}", id.n)
}

// Example 1: typed ids with PhantomData.
fn demo_typed_ids() {
    println!("-- typed ids --");

    let u = Id::<User>::new(1);
    let t = Id::<Team>::new(1);
    println!("{}", load_user(u));

    // load_user(t); // error[E0308]: expected `Id<User>`, found `Id<Team>`
    println!("team id {} size of Id<Team> {} bytes", t.n, size_of::<Id<Team>>());
}

// Example 2: typestate: methods that only exist in certain states.
struct Open;
struct Closed;

struct Door<State> {
    name: String,
    _state: PhantomData<State>,
}

impl Door<Closed> {
    fn new(name: &str) -> Self {
        Door { name: name.into(), _state: PhantomData }
    }

    fn open(self) -> Door<Open> {
        println!("  {} opens", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

impl Door<Open> {
    fn walk_through(&self) {
        println!("  walking through {}", self.name);
    }

    fn close(self) -> Door<Closed> {
        println!("  {} closes", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

fn demo_typestate() {
    println!("-- typestate --");

    let d = Door::new("front");

    // d.walk_through(); // error[E0599]: no method named `walk_through` found for `Door<Closed>`
    let d = d.open();
    d.walk_through();

    let _closed = d.close();
}

// Example 3: units of measure with zero runtime cost.
#[derive(Debug, Clone, Copy)]
struct Meters;
#[derive(Debug, Clone, Copy)]
struct Seconds;

#[derive(Debug, Clone, Copy)]
struct Quantity<U> {
    v: f64,
    _u: PhantomData<U>,
}

impl<U> std::ops::Add for Quantity<U> {
    type Output = Quantity<U>;
    fn add(self, o: Self) -> Self {
        Quantity { v: self.v + o.v, _u: PhantomData }
    }
}

fn demo_units() {
    println!("-- units --");

    let a: Quantity<Meters> = Quantity { v: 3.0, _u: PhantomData };
    let b: Quantity<Meters> = Quantity { v: 4.5, _u: PhantomData };
    let t: Quantity<Seconds> = Quantity { v: 2.0, _u: PhantomData };
    println!("a + b = {} m, t = {} s", (a + b).v, t.v);

    // a + t; // error[E0308]: mismatched types: expected `Quantity<Meters>`, found `Quantity<Seconds>`
}

fn name_len(s: &str) -> usize {
    s.len()
}

fn show<T: Display + ?Sized>(t: &T) -> String {
    format!("<{t}>")
}

fn show_sized<T: Display>(t: &T) -> String {
    format!("[{t}]")
}

// Example 4: Sized and ?Sized.
fn demo_sized() {
    println!("-- Sized / ?Sized --");

    let boxed: Box<str> = "hello".into();
    println!("{} {}", name_len(&boxed), size_of_val(&*boxed));
    println!("{} {}", show("a str"), show(&5)); // T = str is allowed because of ?Sized
    println!("{}", show_sized(&"a &str")); // T = &str, which is Sized
    // show_sized::<str>("x"); // error[E0277]: the size for values of type `str` cannot be known
    let slice: &[i32] = &[1, 2, 3];
    let d: &dyn Debug = &slice;
    println!("unsized values behind pointers: {slice:?} {d:?}");
}

fn describe(value: &dyn Any) -> String {
    if let Some(n) = value.downcast_ref::<i32>() {
        format!("i32 {n}")
    } else if let Some(s) = value.downcast_ref::<String>() {
        format!("String {s:?}")
    } else if value.is::<f64>() {
        "some f64".into()
    } else {
        "unknown".into()
    }
}

// Example 5: Any and downcasting.
fn demo_any() {
    println!("-- Any --");

    println!(
        "{} | {} | {} | {}",
        describe(&7i32),
        describe(&String::from("hi")),
        describe(&1.5f64),
        describe(&'c')
    );

    let items: Vec<Box<dyn Any>> =
        vec![Box::new(1i32), Box::new("str slice"), Box::new(2i32)];
    let ints: i32 = items.iter().filter_map(|b| b.downcast_ref::<i32>()).sum();
    println!("sum of the i32s {ints}");

    let b: Box<dyn Any> = Box::new(String::from("owned"));
    match b.downcast::<String>() {
        Ok(s) => println!("downcast Box to String {s}"),
        Err(_) => println!("not a String"),
    }
}

// Example 6: a type map keyed by TypeId.
struct Resources {
    map: HashMap<TypeId, Box<dyn Any>>,
}

impl Resources {
    fn insert<T: Any>(&mut self, v: T) {
        self.map.insert(TypeId::of::<T>(), Box::new(v));
    }

    fn get<T: Any>(&self) -> Option<&T> {
        self.map.get(&TypeId::of::<T>())?.downcast_ref()
    }
}

#[derive(Debug)]
struct Gravity(f64);

fn demo_type_map() {
    println!("-- type map --");

    let mut r = Resources { map: HashMap::new() };
    r.insert(Gravity(9.81));
    r.insert(String::from("level-1"));
    println!("{:?} {:?} {:?}", r.get::<Gravity>(), r.get::<String>(), r.get::<u8>());
    println!("g = {}", r.get::<Gravity>().map_or(0.0, |g| g.0));
}

// Example 7: type_name and TypeId.
fn demo_type_name() {
    println!("-- type_name / TypeId --");

    println!("{}", type_name::<Vec<i32>>());
    println!("{}", type_name::<Option<&str>>());

    let closure = |x: i32| x + 1;
    println!("of_val {} / {}", type_name_of_val(&1.0f32), type_name_of_val(&closure(1)));
    println!("TypeId equal for same type: {}", TypeId::of::<u8>() == TypeId::of::<u8>());
    println!("TypeId differs: {}", TypeId::of::<u8>() != TypeId::of::<i8>());
}

fn main() {
    demo_typed_ids();
    demo_typestate();
    demo_units();
    demo_sized();
    demo_any();
    demo_type_map();
    demo_type_name();
}