🎓 A1Learner
🔍

Rust Phantom Types

A phantom type parameter is a type label that stores nothing. The label costs nothing at run time.

The pain: plain numbers carry no label

Two lengths as plain f64 values add without complaint, even when one is metres and the other is feet:

fn main() {
    let run_m = 100.0;
    let walk_ft = 10.0;
    println!("total = {}", run_m + walk_ft);
}

A label that stores nothing

A phantom type parameter is a letter in the type name that holds no stored value. Use a PhantomData<Unit> field to tell the compiler the type uses that letter. Empty types make good labels:

struct Metres;
struct Feet;
#[derive(Debug)]
struct Length<Unit>(f64, PhantomData<Unit>);

Same label: adding works

The Add impl is written for Length<Unit>. Both sides share the one Unit. So only same-unit lengths can ever meet at a + sign:

let a: Length<Metres> = Length(100.0, PhantomData);
let b: Length<Metres> = Length(50.0, PhantomData);
println!("total = {} metres", (a + b).0);

Different labels: refused

Length<Metres> and Length<Feet> are different types. Adding them is error E0308 — a mix-up is caught before the program runs:

let run: Length<Metres> = Length(100.0, PhantomData);
let walk: Length<Feet> = Length(10.0, PhantomData);
let total = run + walk;

The label costs nothing

PhantomData is a zero-size marker from std::marker. A Length<Metres> takes exactly as much space as the f64 inside it:

println!("size = {} bytes", std::mem::size_of::<Length<Metres>>());

Example

use std::marker::PhantomData;
use std::ops::Add;
struct Metres;
struct Feet;
#[derive(Debug)]
struct Length<Unit>(f64, PhantomData<Unit>);
impl<Unit> Add for Length<Unit> {
    type Output = Length<Unit>;
    fn add(self, other: Length<Unit>) -> Length<Unit> {
        Length(self.0 + other.0, PhantomData)
    }
}
fn main() {
    let run: Length<Metres> = Length(100.0, PhantomData);
    let run_again: Length<Metres> = Length(100.0, PhantomData);
    let walk: Length<Feet> = Length(10.0, PhantomData);
    let walk_again: Length<Feet> = Length(10.0, PhantomData);
    println!("run total = {} metres", (run + run_again).0);
    println!("walk total = {} feet", (walk + walk_again).0);
    println!("size = {} bytes", std::mem::size_of::<Length<Metres>>());
}

Key points

  • Use phantom labels for units, currencies, or states.
  • A Draft document and a Published document can be one struct with two labels.
  • A phantom type parameter labels a type without storing a value.

In this example, Length stores one f64 and a PhantomData label. run and run_again are both Length<Metres>, so the Add impl accepts them and 100 + 100 prints run total = 200 metres. walk and walk_again are Length<Feet>, and they add the same way, printing 20 feet. A run + walk line would fail with E0308, because the labels differ. The last line proves the label is free. The whole Length<Metres> value is 8 bytes. That is exactly the size of the f64 inside it.

Note

All of this checking happens at compile time.

PhantomData stores nothing and does nothing while the program runs.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Rust Playground

Exercise

What does PhantomData<Unit> store at run time?