ExamplesBy LevelBy TopicLearning Paths
874 Advanced

874-phantom-types — Phantom Types

Functional Programming

Tutorial

The Problem

Type safety is most powerful when it prevents entire classes of bugs at compile time. Phantom types are a technique where a type parameter appears in a struct definition but carries no data — it exists solely to encode information in the type system. The classic applications are units of measure (preventing meters from being added to seconds), state machines (preventing unlocked-door operations when the door is locked), and access levels (preventing unauthorized API calls). F#'s units of measure, Haskell's phantom type pattern, and OCaml's typed phantom parameters all implement this idea. Rust uses PhantomData<T> to hold the phantom type without adding runtime overhead.

🎯 Learning Outcomes

  • • Use PhantomData<T> to encode compile-time invariants without runtime cost
  • • Implement units-of-measure type safety preventing invalid arithmetic
  • • Model a state machine (locked/unlocked door) where invalid transitions are compile errors
  • • Understand why PhantomData is needed in Rust (variance, drop check)
  • • Compare Rust phantom types with OCaml's phantom type parameters
  • Code Example

    struct Meters;
    struct Seconds;
    
    struct Quantity<Unit> {
        value: f64,
        _unit: PhantomData<Unit>,
    }
    
    impl<U> std::ops::Add for Quantity<U> {
        type Output = Self;
        fn add(self, rhs: Self) -> Self { Quantity::new(self.value + rhs.value) }
    }

    Key Differences

  • PhantomData requirement: Rust requires PhantomData<Unit> because unused type parameters are rejected; OCaml type parameters are allowed to be phantom implicitly.
  • Zero-size guarantee: Rust PhantomData<T> is zero-sized; OCaml phantom types also add zero runtime cost.
  • State machine enforcement: Rust can encode that only Door<Unlocked> has an open method via separate impl blocks; OCaml uses module signatures to hide invalid methods.
  • Variance: PhantomData also controls variance (covariant/contravariant) in Rust; OCaml handles variance through its type system automatically.
  • OCaml Approach

    OCaml phantom types use type parameters that are never instantiated: type 'unit quantity = { value: float }. The type meters quantity and seconds quantity are distinct despite having the same runtime representation. OCaml's type checker enforces the distinction. State machines use type unlocked door and type locked door — different phantom instantiations of the same runtime struct. The OCaml approach is more concise but requires discipline since the compiler cannot guarantee exhaustiveness of state transitions.

    Full Source

    #![allow(clippy::all)]
    // Example 080: Phantom Types
    // Compile-time safety with phantom type parameters
    
    use std::marker::PhantomData;
    
    // === Approach 1: Units of measure ===
    struct Meters;
    struct Seconds;
    struct MetersPerSecond;
    
    #[derive(Debug, Clone, Copy)]
    struct Quantity<Unit> {
        value: f64,
        _unit: PhantomData<Unit>,
    }
    
    impl<U> Quantity<U> {
        fn new(value: f64) -> Self {
            Quantity {
                value,
                _unit: PhantomData,
            }
        }
    
        fn scale(self, factor: f64) -> Self {
            Quantity::new(self.value * factor)
        }
    }
    
    // Same-unit addition
    impl<U> std::ops::Add for Quantity<U> {
        type Output = Self;
        fn add(self, rhs: Self) -> Self {
            Quantity::new(self.value + rhs.value)
        }
    }
    
    fn speed(distance: Quantity<Meters>, time: Quantity<Seconds>) -> Quantity<MetersPerSecond> {
        Quantity::new(distance.value / time.value)
    }
    
    // === Approach 2: State machine with phantom types ===
    struct Locked;
    struct Unlocked;
    
    struct Door<State> {
        name: String,
        _state: PhantomData<State>,
    }
    
    impl Door<Unlocked> {
        fn new(name: &str) -> Self {
            Door {
                name: name.to_string(),
                _state: PhantomData,
            }
        }
    
        fn lock(self) -> Door<Locked> {
            Door {
                name: self.name,
                _state: PhantomData,
            }
        }
    
        fn walk_through(&self) -> String {
            format!("Walked through {}", self.name)
        }
    }
    
    impl Door<Locked> {
        fn unlock(self) -> Door<Unlocked> {
            Door {
                name: self.name,
                _state: PhantomData,
            }
        }
        // Cannot walk_through a locked door — method doesn't exist!
    }
    
    // === Approach 3: Validated data ===
    struct Unvalidated;
    struct Validated;
    
    struct Email<State> {
        address: String,
        _state: PhantomData<State>,
    }
    
    impl Email<Unvalidated> {
        fn new(address: &str) -> Self {
            Email {
                address: address.to_string(),
                _state: PhantomData,
            }
        }
    
        fn validate(self) -> Result<Email<Validated>, String> {
            if self.address.contains('@') {
                Ok(Email {
                    address: self.address,
                    _state: PhantomData,
                })
            } else {
                Err(format!("Invalid email: {}", self.address))
            }
        }
    }
    
    impl Email<Validated> {
        fn send(&self) -> String {
            format!("Sent to {}", self.address)
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_quantity_addition() {
            let a = Quantity::<Meters>::new(10.0);
            let b = Quantity::<Meters>::new(20.0);
            let c = a + b;
            assert!((c.value - 30.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_speed_calculation() {
            let d = Quantity::<Meters>::new(100.0);
            let t = Quantity::<Seconds>::new(10.0);
            let s = speed(d, t);
            assert!((s.value - 10.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_scale() {
            let d = Quantity::<Meters>::new(5.0);
            let d2 = d.scale(3.0);
            assert!((d2.value - 15.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_door_state_machine() {
            let door = Door::<Unlocked>::new("test");
            assert_eq!(door.walk_through(), "Walked through test");
            let locked = door.lock();
            let unlocked = locked.unlock();
            assert_eq!(unlocked.walk_through(), "Walked through test");
        }
    
        #[test]
        fn test_valid_email() {
            let email = Email::<Unvalidated>::new("a@b.com");
            let valid = email.validate().unwrap();
            assert_eq!(valid.send(), "Sent to a@b.com");
        }
    
        #[test]
        fn test_invalid_email() {
            let email = Email::<Unvalidated>::new("nope");
            assert!(email.validate().is_err());
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_quantity_addition() {
            let a = Quantity::<Meters>::new(10.0);
            let b = Quantity::<Meters>::new(20.0);
            let c = a + b;
            assert!((c.value - 30.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_speed_calculation() {
            let d = Quantity::<Meters>::new(100.0);
            let t = Quantity::<Seconds>::new(10.0);
            let s = speed(d, t);
            assert!((s.value - 10.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_scale() {
            let d = Quantity::<Meters>::new(5.0);
            let d2 = d.scale(3.0);
            assert!((d2.value - 15.0).abs() < 1e-10);
        }
    
        #[test]
        fn test_door_state_machine() {
            let door = Door::<Unlocked>::new("test");
            assert_eq!(door.walk_through(), "Walked through test");
            let locked = door.lock();
            let unlocked = locked.unlock();
            assert_eq!(unlocked.walk_through(), "Walked through test");
        }
    
        #[test]
        fn test_valid_email() {
            let email = Email::<Unvalidated>::new("a@b.com");
            let valid = email.validate().unwrap();
            assert_eq!(valid.send(), "Sent to a@b.com");
        }
    
        #[test]
        fn test_invalid_email() {
            let email = Email::<Unvalidated>::new("nope");
            assert!(email.validate().is_err());
        }
    }

    Deep Comparison

    Comparison: Phantom Types

    Units of Measure

    OCaml:

    type meters
    type seconds
    type 'a quantity = { value : float }
    
    let meters v : meters quantity = { value = v }
    let seconds v : seconds quantity = { value = v }
    
    let add_same (a : 'a quantity) (b : 'a quantity) : 'a quantity =
      { value = a.value +. b.value }
    

    Rust:

    struct Meters;
    struct Seconds;
    
    struct Quantity<Unit> {
        value: f64,
        _unit: PhantomData<Unit>,
    }
    
    impl<U> std::ops::Add for Quantity<U> {
        type Output = Self;
        fn add(self, rhs: Self) -> Self { Quantity::new(self.value + rhs.value) }
    }
    

    State Machine

    OCaml:

    type unlocked
    type locked
    type 'state door = { name : string }
    
    let lock (d : unlocked door) : locked door = { name = d.name }
    let walk_through (d : unlocked door) = Printf.sprintf "Walked through %s" d.name
    

    Rust:

    struct Door<State> { name: String, _state: PhantomData<State> }
    
    impl Door<Unlocked> {
        fn lock(self) -> Door<Locked> { Door { name: self.name, _state: PhantomData } }
        fn walk_through(&self) -> String { format!("Walked through {}", self.name) }
    }
    // Door<Locked> has no walk_through — won't compile!
    

    Validated Data

    OCaml:

    type 'a email = Email of string
    let validate_email (Email s : unvalidated email) : validated email option =
      if String.contains s '@' then Some (Email s) else None
    let send_email (Email s : validated email) = Printf.sprintf "Sent to %s" s
    

    Rust:

    impl Email<Unvalidated> {
        fn validate(self) -> Result<Email<Validated>, String> { /* ... */ }
    }
    impl Email<Validated> {
        fn send(&self) -> String { format!("Sent to {}", self.address) }
    }
    

    Exercises

  • Add a Kilograms unit and implement force(mass: Quantity<Kilograms>, acceleration: Quantity<MetersPerSecond>) -> Quantity<Newtons>.
  • Add an Ajar state to the door state machine and define legal transitions: Unlocked -> Ajar -> Locked.
  • Implement a typed API key wrapper ApiKey<Permission> where ReadOnly and ReadWrite permissions grant different method sets.
  • Open Source Repos