ExamplesBy LevelBy TopicLearning Paths
132 Advanced

Phantom Units of Measure

Functional Programming

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

  • โ€ข Understand how PhantomData<Unit> attaches type-level information without runtime cost
  • โ€ข Learn to implement unit-safe arithmetic via operator overloading (Add, Mul)
  • โ€ข See how unit multiplication produces derived units (e.g., Meters / Seconds = MetersPerSecond)
  • โ€ข Appreciate zero-overhead type safety: Quantity<U> has identical memory layout to f64
  • Code 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

  • Operator restriction: Rust's trait system only allows + 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.
  • Derived units: Rust can encode Meters * Seconds = MeterSeconds via Mul trait bounds; OCaml requires explicit conversion functions.
  • Zero overhead: size_of::<Quantity<Meters>>() == size_of::<f64>() in Rust โ€” the unit tag is pure type information; same in OCaml (transparent type alias).
  • Conversion safety: Rust requires explicit 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    ConceptOCamlRust
    Phantom type wrappertype 'unit quantity = { value: float }struct Quantity<Unit> { value: f64, _unit: PhantomData<Unit> }
    Unit tagAbstract type: type metersZero-sized struct: struct Meters;
    Same-unit additionval add : 'u quantity -> 'u quantity -> 'u quantityimpl<U> Add for Quantity<U>
    Dimensional productRequires separate type annotationimpl Div<Quantity<Seconds>> for Quantity<Meters> โ†’ Quantity<MetersPerSecond>
    Runtime cost{ value: float } โ€” one floatstruct Quantity<Unit> { value: f64, _unit: PhantomData<Unit> } โ€” one f64 (PhantomData is zero bytes)

    Key Insights

  • Phantom means "in the type, not in the data": In OCaml the phantom parameter '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.
  • **Dimensional analysis via 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.
  • No implicit coercions: In both languages, 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.
  • Zero runtime overhead: Both OCaml and Rust compile phantom types away entirely. Quantity<Meters> and Quantity<Feet> are both just a single float/f64 in memory. The unit tags exist only during type-checking.
  • Operator overloading extends the safety net: Rust's trait system lets + 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

  • Add a Quantity<Kilograms> type and implement Mul<Quantity<MetersPerSecond>> yielding Quantity<NewtonSeconds> (impulse = mass ร— velocity).
  • Write a convert_feet_to_meters(q: Quantity<Feet>) -> Quantity<Meters> function and verify that the result cannot be mistakenly added to a Quantity<Feet>.
  • Implement PartialOrd for Quantity<U> and write a min_distance function that takes two Quantity<Meters> values.
  • Open Source Repos