Phantom Units of Measure
Tutorial
The Problem
The Mars Climate Orbiter was lost in 1999 because one team used metric units and another used imperial โ the mismatch went undetected until the spacecraft disintegrated. Unit confusion bugs kill software. Phantom types solve this: tag numeric values with their unit at the type level so Quantity<Meters> and Quantity<Feet> are distinct, incompatible types. Adding meters to feet is a compile error, not a runtime surprise. This pattern is used in aerospace, physics simulations, and financial systems.
🎯 Learning Outcomes
PhantomData<Unit> attaches type-level information without runtime costAdd, Mul)Meters / Seconds = MetersPerSecond)Quantity<U> has identical memory layout to f64Code Example
use std::marker::PhantomData;
use std::ops::{Add, Div, Mul};
pub struct Meters;
pub struct Seconds;
pub struct MetersPerSecond;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Quantity<U> {
pub fn new(value: f64) -> Self {
Quantity { value, _unit: PhantomData }
}
pub fn value(self) -> f64 { self.value }
}
// Same-unit addition โ only compiles when both sides match
impl<U> Add for Quantity<U> {
type Output = Quantity<U>;
fn add(self, rhs: Self) -> Self::Output {
Quantity::new(self.value + rhs.value)
}
}
// Dimensional analysis: Meters / Seconds = MetersPerSecond
impl Div<Quantity<Seconds>> for Quantity<Meters> {
type Output = Quantity<MetersPerSecond>;
fn div(self, rhs: Quantity<Seconds>) -> Self::Output {
Quantity::new(self.value / rhs.value)
}
}Key Differences
+ between Quantity<U> and Quantity<U> โ cross-unit addition is a compile error; OCaml's float operators work on all _ quantity values regardless of tag.Meters * Seconds = MeterSeconds via Mul trait bounds; OCaml requires explicit conversion functions.size_of::<Quantity<Meters>>() == size_of::<f64>() in Rust โ the unit tag is pure type information; same in OCaml (transparent type alias).from_feet_to_meters conversion functions; OCaml similarly requires them, but the type system provides weaker enforcement.OCaml Approach
OCaml can simulate phantom units using polymorphic types:
type meters
type feet
type 'unit quantity = float
let meters (x: float) : meters quantity = x
let feet (x: float) : feet quantity = x
(* let _ = meters 5.0 +. feet 3.0 (* type error *) *)
This is lighter syntactically but provides less safety โ arithmetic operators on float still work regardless of the phantom tag because OCaml's type aliases are transparent to the operator implementations.
Full Source
#![allow(clippy::all)]
//! # Example 132: Phantom Units of Measure
//!
//! Tag numeric values with their unit of measure so the compiler prevents
//! accidental mixing of incompatible units โ e.g., adding metres to feet
//! or passing a duration where a distance is expected.
use std::marker::PhantomData;
use std::ops::{Add, Div, Mul};
// ---------------------------------------------------------------------------
// Unit marker types (zero-sized; never stored in memory)
// ---------------------------------------------------------------------------
/// Approach 1: Phantom type units โ simple marker structs.
/// Must be `Clone + Copy` so that `#[derive(Clone, Copy)]` on `Quantity<U>`
/// applies for all concrete unit types.
#[derive(Debug, Clone, Copy)]
pub struct Meters;
#[derive(Debug, Clone, Copy)]
pub struct Feet;
#[derive(Debug, Clone, Copy)]
pub struct Seconds;
#[derive(Debug, Clone, Copy)]
pub struct Kilograms;
#[derive(Debug, Clone, Copy)]
pub struct MetersPerSecond;
#[derive(Debug, Clone, Copy)]
pub struct NewtonSeconds; // impulse = kgยทm/s
// ---------------------------------------------------------------------------
// Quantity<Unit> โ wraps f64 + phantom unit tag
// ---------------------------------------------------------------------------
/// A numeric quantity tagged with a compile-time unit.
///
/// `PhantomData<Unit>` costs zero bytes at runtime but participates fully
/// in the type system, making `Quantity<Meters>` and `Quantity<Feet>`
/// distinct, incompatible types.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Quantity<U> {
/// Construct a quantity with the given value and unit inferred from context.
pub fn new(value: f64) -> Self {
Quantity {
value,
_unit: PhantomData,
}
}
/// Extract the raw numeric value.
pub fn value(self) -> f64 {
self.value
}
/// Scale by a dimensionless factor (same unit).
pub fn scale(self, factor: f64) -> Self {
Quantity::new(self.value * factor)
}
}
// ---------------------------------------------------------------------------
// Convenience constructors (mirror the OCaml `meters`, `seconds`, โฆ helpers)
// ---------------------------------------------------------------------------
pub fn meters(v: f64) -> Quantity<Meters> {
Quantity::new(v)
}
pub fn feet(v: f64) -> Quantity<Feet> {
Quantity::new(v)
}
pub fn seconds(v: f64) -> Quantity<Seconds> {
Quantity::new(v)
}
pub fn kilograms(v: f64) -> Quantity<Kilograms> {
Quantity::new(v)
}
// ---------------------------------------------------------------------------
// Operator impls
// ---------------------------------------------------------------------------
/// Same-unit addition โ only compiles when both sides share the same `Unit`.
impl<U> Add for Quantity<U> {
type Output = Quantity<U>;
fn add(self, rhs: Self) -> Self::Output {
Quantity::new(self.value + rhs.value)
}
}
/// Scalar multiplication โ keeps the same unit.
impl<U> Mul<f64> for Quantity<U> {
type Output = Quantity<U>;
fn mul(self, rhs: f64) -> Self::Output {
Quantity::new(self.value * rhs)
}
}
// ---------------------------------------------------------------------------
// Physics relationships โ type-checked dimensional analysis
// ---------------------------------------------------------------------------
/// distance / time โ speed (`Meters / Seconds = MetersPerSecond`)
impl Div<Quantity<Seconds>> for Quantity<Meters> {
type Output = Quantity<MetersPerSecond>;
fn div(self, rhs: Quantity<Seconds>) -> Self::Output {
Quantity::new(self.value / rhs.value)
}
}
/// momentum: mass ร velocity โ `Kilograms ยท MetersPerSecond = NewtonSeconds`
impl Mul<Quantity<MetersPerSecond>> for Quantity<Kilograms> {
type Output = Quantity<NewtonSeconds>;
fn mul(self, rhs: Quantity<MetersPerSecond>) -> Self::Output {
Quantity::new(self.value * rhs.value)
}
}
// ---------------------------------------------------------------------------
// Approach 2: Unit conversion (explicit, type-safe)
// ---------------------------------------------------------------------------
/// Convert feet to meters โ produces a `Quantity<Meters>`.
/// The compiler forces you to call this explicitly; there is no implicit coercion.
pub fn feet_to_meters(q: Quantity<Feet>) -> Quantity<Meters> {
Quantity::new(q.value * 0.3048)
}
/// Convert meters to feet.
pub fn meters_to_feet(q: Quantity<Meters>) -> Quantity<Feet> {
Quantity::new(q.value / 0.3048)
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// --- same-unit arithmetic ---
#[test]
fn test_add_same_unit() {
let a = meters(3.0);
let b = meters(4.0);
assert_eq!((a + b).value(), 7.0);
}
#[test]
fn test_scale() {
let d = meters(5.0);
assert_eq!(d.scale(2.0).value(), 10.0);
}
#[test]
fn test_mul_scalar() {
let d = seconds(4.0);
assert_eq!((d * 3.0).value(), 12.0);
}
// --- dimensional analysis ---
#[test]
fn test_speed_from_distance_over_time() {
let dist = meters(100.0);
let time = seconds(10.0);
let speed: Quantity<MetersPerSecond> = dist / time;
assert_eq!(speed.value(), 10.0);
}
#[test]
fn test_momentum_kg_times_mps() {
let mass = kilograms(70.0);
let speed: Quantity<MetersPerSecond> = Quantity::new(9.0);
let momentum: Quantity<NewtonSeconds> = mass * speed;
assert_eq!(momentum.value(), 630.0);
}
// --- unit conversion ---
#[test]
fn test_feet_to_meters() {
let one_foot = feet(1.0);
let in_meters = feet_to_meters(one_foot);
let diff = (in_meters.value() - 0.3048).abs();
assert!(diff < 1e-10, "expected โ0.3048, got {}", in_meters.value());
}
#[test]
fn test_meters_to_feet_roundtrip() {
let original = meters(10.0);
let roundtripped = feet_to_meters(meters_to_feet(original));
let diff = (roundtripped.value() - original.value()).abs();
assert!(diff < 1e-10);
}
#[test]
fn test_feet_addition_stays_feet() {
let a = feet(3.0);
let b = feet(4.0);
// result is Quantity<Feet>, not Quantity<Meters>
let sum: Quantity<Feet> = a + b;
assert_eq!(sum.value(), 7.0);
}
// --- zero / negative values ---
#[test]
fn test_zero_quantity() {
let zero = meters(0.0);
assert_eq!((zero + meters(5.0)).value(), 5.0);
}
#[test]
fn test_negative_quantity() {
let a = seconds(-3.0);
let b = seconds(3.0);
assert_eq!((a + b).value(), 0.0);
}
}#[cfg(test)]
mod tests {
use super::*;
// --- same-unit arithmetic ---
#[test]
fn test_add_same_unit() {
let a = meters(3.0);
let b = meters(4.0);
assert_eq!((a + b).value(), 7.0);
}
#[test]
fn test_scale() {
let d = meters(5.0);
assert_eq!(d.scale(2.0).value(), 10.0);
}
#[test]
fn test_mul_scalar() {
let d = seconds(4.0);
assert_eq!((d * 3.0).value(), 12.0);
}
// --- dimensional analysis ---
#[test]
fn test_speed_from_distance_over_time() {
let dist = meters(100.0);
let time = seconds(10.0);
let speed: Quantity<MetersPerSecond> = dist / time;
assert_eq!(speed.value(), 10.0);
}
#[test]
fn test_momentum_kg_times_mps() {
let mass = kilograms(70.0);
let speed: Quantity<MetersPerSecond> = Quantity::new(9.0);
let momentum: Quantity<NewtonSeconds> = mass * speed;
assert_eq!(momentum.value(), 630.0);
}
// --- unit conversion ---
#[test]
fn test_feet_to_meters() {
let one_foot = feet(1.0);
let in_meters = feet_to_meters(one_foot);
let diff = (in_meters.value() - 0.3048).abs();
assert!(diff < 1e-10, "expected โ0.3048, got {}", in_meters.value());
}
#[test]
fn test_meters_to_feet_roundtrip() {
let original = meters(10.0);
let roundtripped = feet_to_meters(meters_to_feet(original));
let diff = (roundtripped.value() - original.value()).abs();
assert!(diff < 1e-10);
}
#[test]
fn test_feet_addition_stays_feet() {
let a = feet(3.0);
let b = feet(4.0);
// result is Quantity<Feet>, not Quantity<Meters>
let sum: Quantity<Feet> = a + b;
assert_eq!(sum.value(), 7.0);
}
// --- zero / negative values ---
#[test]
fn test_zero_quantity() {
let zero = meters(0.0);
assert_eq!((zero + meters(5.0)).value(), 5.0);
}
#[test]
fn test_negative_quantity() {
let a = seconds(-3.0);
let b = seconds(3.0);
assert_eq!((a + b).value(), 0.0);
}
}
Deep Comparison
OCaml vs Rust: Phantom Units of Measure
Side-by-Side Code
OCaml
(* Abstract type tags โ phantom; no constructors exposed *)
type meters
type seconds
type kilograms
(* The phantom type parameter 'unit is never stored *)
type 'unit quantity = { value : float }
let meters v : meters quantity = { value = v }
let seconds v : seconds quantity = { value = v }
let add (a : 'u quantity) (b : 'u quantity) : 'u quantity =
{ value = a.value +. b.value }
(* distance / time โ speed โ no separate speed type in simple version *)
let speed_of dist time : float =
dist.value /. time.value
Rust (idiomatic โ operator overloading)
use std::marker::PhantomData;
use std::ops::{Add, Div, Mul};
pub struct Meters;
pub struct Seconds;
pub struct MetersPerSecond;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Quantity<Unit> {
value: f64,
_unit: PhantomData<Unit>,
}
impl<U> Quantity<U> {
pub fn new(value: f64) -> Self {
Quantity { value, _unit: PhantomData }
}
pub fn value(self) -> f64 { self.value }
}
// Same-unit addition โ only compiles when both sides match
impl<U> Add for Quantity<U> {
type Output = Quantity<U>;
fn add(self, rhs: Self) -> Self::Output {
Quantity::new(self.value + rhs.value)
}
}
// Dimensional analysis: Meters / Seconds = MetersPerSecond
impl Div<Quantity<Seconds>> for Quantity<Meters> {
type Output = Quantity<MetersPerSecond>;
fn div(self, rhs: Quantity<Seconds>) -> Self::Output {
Quantity::new(self.value / rhs.value)
}
}
Rust (functional style โ explicit helper functions)
pub fn meters(v: f64) -> Quantity<Meters> { Quantity::new(v) }
pub fn seconds(v: f64) -> Quantity<Seconds> { Quantity::new(v) }
// Explicit conversion โ no implicit coercion
pub fn feet_to_meters(q: Quantity<Feet>) -> Quantity<Meters> {
Quantity::new(q.value() * 0.3048)
}
Type Signatures
| Concept | OCaml | Rust |
|---|---|---|
| Phantom type wrapper | type 'unit quantity = { value: float } | struct Quantity<Unit> { value: f64, _unit: PhantomData<Unit> } |
| Unit tag | Abstract type: type meters | Zero-sized struct: struct Meters; |
| Same-unit addition | val add : 'u quantity -> 'u quantity -> 'u quantity | impl<U> Add for Quantity<U> |
| Dimensional product | Requires separate type annotation | impl Div<Quantity<Seconds>> for Quantity<Meters> โ Quantity<MetersPerSecond> |
| Runtime cost | { value: float } โ one float | struct Quantity<Unit> { value: f64, _unit: PhantomData<Unit> } โ one f64 (PhantomData is zero bytes) |
Key Insights
'unit never appears in the record fields. In Rust, PhantomData<Unit> is the explicit carrier โ it is a zero-sized type that satisfies the compiler's "every type parameter must be used" rule without adding any runtime storage.impl blocks**: OCaml's simple version just returns float for mixed-unit operations, losing the unit information. Rust lets you express Meters / Seconds โ MetersPerSecond as a concrete Div impl that the compiler enforces โ wrong-unit division is a compile error.Quantity<Meters> and Quantity<Feet> are structurally identical at runtime yet statically incompatible. Conversion between them must be explicit (feet_to_meters). The compiler refuses to accept one where the other is expected โ exactly the safety guarantee that could have saved the Mars Climate Orbiter.Quantity<Meters> and Quantity<Feet> are both just a single float/f64 in memory. The unit tags exist only during type-checking.+ remain intuitive while being unit-safe โ meters(3.0) + meters(4.0) compiles, but meters(3.0) + feet(4.0) does not. OCaml achieves the same via the type of add, but without operator syntax.When to Use Each Style
Use idiomatic Rust (operator overloading) when building a library that end users interact with through natural arithmetic expressions โ the +, /, * syntax keeps calling code readable while the type system enforces correctness invisibly.
Use explicit constructor functions (meters(), seconds()) at API boundaries to make the unit annotation visible in the source code, reducing the chance of a caller accidentally constructing a value with the wrong unit.
Exercises
Quantity<Kilograms> type and implement Mul<Quantity<MetersPerSecond>> yielding Quantity<NewtonSeconds> (impulse = mass ร velocity).convert_feet_to_meters(q: Quantity<Feet>) -> Quantity<Meters> function and verify that the result cannot be mistakenly added to a Quantity<Feet>.PartialOrd for Quantity<U> and write a min_distance function that takes two Quantity<Meters> values.