ExamplesBy LevelBy TopicLearning Paths
100 Advanced

100 — Phantom Types

Functional Programming

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

  • • Use PhantomData<T> to add a type parameter that exists only at compile time
  • • Understand that PhantomData<T> is zero-sized: no runtime cost
  • • Implement Add<Quantity<U>> for Quantity<U> to allow same-unit addition
  • • Understand why you cannot add Quantity<Meters> to Quantity<Seconds> — different U
  • • Map Rust's PhantomData to OCaml's abstract type meters phantom type
  • • Compare with the simpler newtype pattern for when phantom types are overkill
  • Code 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

    AspectRustOCaml
    MarkerPhantomData<Unit> fieldtype meters empty type
    Tagged valueQuantity<Meters>meters quantity
    Unit-safe addimpl Add for Quantity<U>add : 'a quantity -> 'a quantity -> 'a quantity
    Runtime costZeroZero
    Compile errorType mismatch on UType mismatch on 'a
    AlternativeNewtype 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);
        }
    }
    ✓ Tests Rust test suite
    #[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

    AspectOCamlRust
    Phantom typetype meters (abstract)struct Meters; (zero-sized)
    MarkerImplicit in type paramPhantomData<Unit>
    Runtime costZeroZero (PhantomData is ZST)
    Operator overloadFunctions onlyimpl Add for Quantity<U>
    AlternativeModule signaturesNewtype wrappers
    Size checkN/Asize_of::<Quantity<M>>() == size_of::<f64>()

    Learner Notes

  • • **PhantomData**: Rust requires explicit marking because it tracks all type parameters for drop checking and variance
  • Zero-sized types (ZSTs): struct Meters; takes 0 bytes — the compiler optimizes it away entirely
  • Trait-based ops: Rust's impl Add gives + syntax; OCaml just uses named functions
  • Newtype alternative: struct Meters(f64) is simpler but requires implementing ops for each unit separately
  • OCaml's elegance: Abstract types + type annotations is more concise than Rust's PhantomData approach
  • Exercises

  • Add a MetersPerSecond unit type and implement fn speed(d: Quantity<Meters>, t: Quantity<Seconds>) -> Quantity<MetersPerSecond>.
  • Implement impl Mul<f64> for Quantity<U> and impl Div<Quantity<U>> for Quantity<U> returning a dimensionless f64.
  • Extend to a Quantity<Unit, Currency> with two phantom parameters for monetary amounts in different currencies.
  • Implement a typestate builder: Connection<Disconnected>Connection<Connected> that prevents calling send before connect.
  • In OCaml, implement the same typestate pattern using phantom types and GADTs (generalised algebraic data types).
  • Open Source Repos