100 — Phantom Types
Tutorial Video
Text description (accessibility)
This video demonstrates the "100 — Phantom Types" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Use `PhantomData<Unit>` to tag a `Quantity<Unit>` struct with a unit type, preventing addition of meters to seconds at compile time. Key difference from OCaml: | Aspect | Rust | OCaml |
Tutorial
The Problem
Use PhantomData<Unit> to tag a Quantity<Unit> struct with a unit type, preventing addition of meters to seconds at compile time. The unit type Meters or Seconds carries no runtime data — it exists purely as a type-level marker. Compare with OCaml's abstract phantom types (type meters) and the simpler newtype alternative.
🎯 Learning Outcomes
PhantomData<T> to add a type parameter that exists only at compile timePhantomData<T> is zero-sized: no runtime costAdd<Quantity<U>> for Quantity<U> to allow same-unit additionQuantity<Meters> to Quantity<Seconds> — different UPhantomData to OCaml's abstract type meters phantom typeCode Example
use std::marker::PhantomData;
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Add for Quantity<U> {
type Output = Self;
fn add(self, rhs: Self) -> Self { Quantity::new(self.value + rhs.value) }
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Marker | PhantomData<Unit> field | type meters empty type |
| Tagged value | Quantity<Meters> | meters quantity |
| Unit-safe add | impl Add for Quantity<U> | add : 'a quantity -> 'a quantity -> 'a quantity |
| Runtime cost | Zero | Zero |
| Compile error | Type mismatch on U | Type mismatch on 'a |
| Alternative | Newtype struct MetersVal(f64) | Same: single-constructor variant |
Phantom types are a power tool for encoding invariants in the type system. They are used in Rust for: typestate machines (File<Open> vs File<Closed>), brand types (preventing index confusion), and units of measure. When a simpler newtype suffices, prefer it.
OCaml Approach
OCaml uses type meters (empty abstract type) and type 'a quantity = Q of float. meters x : meters quantity and seconds x : seconds quantity create tagged values. add (Q a : 'a quantity) (Q b : 'a quantity) enforces same-unit addition at the type level. The 'a phantom parameter makes this work with the same elegance as Rust's PhantomData, but with less syntactic overhead.
Full Source
#![allow(clippy::all)]
//! # Phantom Types — Type-Safe Units
//!
//! Use phantom type parameters to prevent mixing meters and seconds at compile time.
//! OCaml's abstract types map to Rust's `PhantomData<T>` marker.
use std::marker::PhantomData;
use std::ops::Add;
// ---------------------------------------------------------------------------
// Approach A: PhantomData marker (idiomatic Rust)
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy)]
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
#[derive(Debug)]
pub struct Meters;
#[derive(Debug)]
pub struct Seconds;
impl<U> Quantity<U> {
pub fn new(value: f64) -> Self {
Quantity {
value,
_unit: PhantomData,
}
}
pub fn value(&self) -> f64 {
self.value
}
pub fn scale(&self, k: f64) -> Self {
Quantity::new(k * self.value)
}
}
impl<U> Add for Quantity<U> {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Quantity::new(self.value + rhs.value)
}
}
pub fn meters(v: f64) -> Quantity<Meters> {
Quantity::new(v)
}
pub fn seconds(v: f64) -> Quantity<Seconds> {
Quantity::new(v)
}
// ---------------------------------------------------------------------------
// Approach B: Newtype wrappers (simpler, no PhantomData)
// ---------------------------------------------------------------------------
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MetersVal(pub f64);
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SecondsVal(pub f64);
impl Add for MetersVal {
type Output = Self;
fn add(self, rhs: Self) -> Self {
MetersVal(self.0 + rhs.0)
}
}
impl Add for SecondsVal {
type Output = Self;
fn add(self, rhs: Self) -> Self {
SecondsVal(self.0 + rhs.0)
}
}
// ---------------------------------------------------------------------------
// Approach C: Const generics (Rust-specific, experimental flavor)
// ---------------------------------------------------------------------------
// Using a string-based unit tag with const generics is nightly-only,
// but the concept shows Rust's direction for compile-time unit checking.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_same_units() {
let d1 = meters(100.0);
let d2 = meters(50.0);
let total = d1 + d2;
assert!((total.value() - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_scale() {
let t = seconds(3.0);
let doubled = t.scale(2.0);
assert!((doubled.value() - 6.0).abs() < f64::EPSILON);
}
// This should NOT compile — uncomment to verify:
// #[test]
// fn test_add_different_units_fails() {
// let d = meters(100.0);
// let t = seconds(5.0);
// let _ = d + t; // Compile error!
// }
#[test]
fn test_newtype_add() {
assert_eq!(MetersVal(10.0) + MetersVal(5.0), MetersVal(15.0));
}
#[test]
fn test_phantom_zero_size() {
assert_eq!(
std::mem::size_of::<Quantity<Meters>>(),
std::mem::size_of::<f64>()
);
}
#[test]
fn test_multiple_operations() {
let d = meters(10.0) + meters(20.0);
let scaled = d.scale(3.0);
assert!((scaled.value() - 90.0).abs() < f64::EPSILON);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_same_units() {
let d1 = meters(100.0);
let d2 = meters(50.0);
let total = d1 + d2;
assert!((total.value() - 150.0).abs() < f64::EPSILON);
}
#[test]
fn test_scale() {
let t = seconds(3.0);
let doubled = t.scale(2.0);
assert!((doubled.value() - 6.0).abs() < f64::EPSILON);
}
// This should NOT compile — uncomment to verify:
// #[test]
// fn test_add_different_units_fails() {
// let d = meters(100.0);
// let t = seconds(5.0);
// let _ = d + t; // Compile error!
// }
#[test]
fn test_newtype_add() {
assert_eq!(MetersVal(10.0) + MetersVal(5.0), MetersVal(15.0));
}
#[test]
fn test_phantom_zero_size() {
assert_eq!(
std::mem::size_of::<Quantity<Meters>>(),
std::mem::size_of::<f64>()
);
}
#[test]
fn test_multiple_operations() {
let d = meters(10.0) + meters(20.0);
let scaled = d.scale(3.0);
assert!((scaled.value() - 90.0).abs() < f64::EPSILON);
}
}
Deep Comparison
Comparison: Phantom Types — OCaml vs Rust
Core Insight
Phantom types demonstrate zero-cost type safety in both languages. OCaml uses abstract types (declared but never defined) as phantom parameters. Rust uses PhantomData<T> — a zero-sized type from std::marker. In both cases, the compiler enforces that you can't add meters to seconds, but the runtime representation is just a float.
OCaml
type meters
type seconds
type 'a quantity = Q of float
let meters x : meters quantity = Q x
let add (Q a : 'a quantity) (Q b : 'a quantity) : 'a quantity = Q (a +. b)
Rust
use std::marker::PhantomData;
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Add for Quantity<U> {
type Output = Self;
fn add(self, rhs: Self) -> Self { Quantity::new(self.value + rhs.value) }
}
Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| Phantom type | type meters (abstract) | struct Meters; (zero-sized) |
| Marker | Implicit in type param | PhantomData<Unit> |
| Runtime cost | Zero | Zero (PhantomData is ZST) |
| Operator overload | Functions only | impl Add for Quantity<U> |
| Alternative | Module signatures | Newtype wrappers |
| Size check | N/A | size_of::<Quantity<M>>() == size_of::<f64>() |
Learner Notes
PhantomData**: Rust requires explicit marking because it tracks all type parameters for drop checking and variancestruct Meters; takes 0 bytes — the compiler optimizes it away entirelyimpl Add gives + syntax; OCaml just uses named functionsstruct Meters(f64) is simpler but requires implementing ops for each unit separatelyExercises
MetersPerSecond unit type and implement fn speed(d: Quantity<Meters>, t: Quantity<Seconds>) -> Quantity<MetersPerSecond>.impl Mul<f64> for Quantity<U> and impl Div<Quantity<U>> for Quantity<U> returning a dimensionless f64.Quantity<Unit, Currency> with two phantom parameters for monetary amounts in different currencies.Connection<Disconnected> → Connection<Connected> that prevents calling send before connect.