ExamplesBy LevelBy TopicLearning Paths
738 Advanced

738-phantom-type-basics — Phantom Type Basics

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "738-phantom-type-basics — Phantom Type Basics" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. Sometimes you need a type to carry extra compile-time information that has no runtime representation. Key difference from OCaml: 1. **Syntax**: Rust requires `PhantomData<Tag>` as an explicit field; OCaml's phantom variables appear naturally in type signatures without a dummy field.

Tutorial

The Problem

Sometimes you need a type to carry extra compile-time information that has no runtime representation. A UserId and a ProductId are both u64, but mixing them up is a logic error. Phantom types solve this: Tagged<u64, UserTag> and Tagged<u64, ProductTag> are different types at compile time but identical at runtime. This pattern prevents unit confusion (meters vs. feet), validates data provenance (raw vs. validated input), and creates marker-based permission systems — all at zero runtime cost.

🎯 Learning Outcomes

  • • Use PhantomData<Tag> to carry type-level information without runtime overhead
  • • Create a Tagged<T, Tag> wrapper that makes distinct "branded" types from the same value type
  • • Model validation state with Validated and Unvalidated markers on UserId
  • • Understand why PhantomData is necessary for the compiler to accept unused type parameters
  • • See how phantom types prevent mixing IDs of different domain entities
  • Code Example

    #![allow(clippy::all)]
    //! # Phantom Type Basics
    //! PhantomData for type-level information
    
    use std::marker::PhantomData;
    
    /// Wrapper with phantom type parameter
    pub struct Tagged<T, Tag> {
        pub value: T,
        _tag: PhantomData<Tag>,
    }
    
    impl<T, Tag> Tagged<T, Tag> {
        pub fn new(value: T) -> Self {
            Tagged {
                value,
                _tag: PhantomData,
            }
        }
        pub fn into_inner(self) -> T {
            self.value
        }
    }
    
    /// Type-level markers
    pub struct Validated;
    pub struct Unvalidated;
    
    /// ID that tracks validation status
    pub struct UserId<State>(u64, PhantomData<State>);
    
    impl UserId<Unvalidated> {
        pub fn new(id: u64) -> Self {
            UserId(id, PhantomData)
        }
        pub fn validate(self) -> Option<UserId<Validated>> {
            if self.0 > 0 {
                Some(UserId(self.0, PhantomData))
            } else {
                None
            }
        }
    }
    
    impl UserId<Validated> {
        pub fn get(&self) -> u64 {
            self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_tagged() {
            struct MyTag;
            let t: Tagged<i32, MyTag> = Tagged::new(42);
            assert_eq!(t.into_inner(), 42);
        }
        #[test]
        fn test_validated() {
            let id = UserId::new(123);
            let validated = id.validate().unwrap();
            assert_eq!(validated.get(), 123);
        }
    }

    Key Differences

  • Syntax: Rust requires PhantomData<Tag> as an explicit field; OCaml's phantom variables appear naturally in type signatures without a dummy field.
  • Encapsulation: Both use module/crate boundaries to prevent construction of phantom-typed values without going through the designated constructor.
  • Multiple phantoms: Rust can combine multiple phantom parameters in a tuple PhantomData<(A, B)>; OCaml uses multiple type variables directly.
  • Performance: Both are zero-cost — PhantomData<T> is zero bytes; OCaml's phantom variables add no runtime overhead.
  • OCaml Approach

    OCaml phantom types use type variables in a similar position: type ('state) user_id = UserId of int64. Modules provide encapsulation: only the module that implements validate can construct Validated user_id. OCaml 5 adds [@@unboxed] to eliminate even the boxing overhead. Jane Street's Id module uses this exact pattern for all entity IDs in their trading systems.

    Full Source

    #![allow(clippy::all)]
    //! # Phantom Type Basics
    //! PhantomData for type-level information
    
    use std::marker::PhantomData;
    
    /// Wrapper with phantom type parameter
    pub struct Tagged<T, Tag> {
        pub value: T,
        _tag: PhantomData<Tag>,
    }
    
    impl<T, Tag> Tagged<T, Tag> {
        pub fn new(value: T) -> Self {
            Tagged {
                value,
                _tag: PhantomData,
            }
        }
        pub fn into_inner(self) -> T {
            self.value
        }
    }
    
    /// Type-level markers
    pub struct Validated;
    pub struct Unvalidated;
    
    /// ID that tracks validation status
    pub struct UserId<State>(u64, PhantomData<State>);
    
    impl UserId<Unvalidated> {
        pub fn new(id: u64) -> Self {
            UserId(id, PhantomData)
        }
        pub fn validate(self) -> Option<UserId<Validated>> {
            if self.0 > 0 {
                Some(UserId(self.0, PhantomData))
            } else {
                None
            }
        }
    }
    
    impl UserId<Validated> {
        pub fn get(&self) -> u64 {
            self.0
        }
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_tagged() {
            struct MyTag;
            let t: Tagged<i32, MyTag> = Tagged::new(42);
            assert_eq!(t.into_inner(), 42);
        }
        #[test]
        fn test_validated() {
            let id = UserId::new(123);
            let validated = id.validate().unwrap();
            assert_eq!(validated.get(), 123);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
        #[test]
        fn test_tagged() {
            struct MyTag;
            let t: Tagged<i32, MyTag> = Tagged::new(42);
            assert_eq!(t.into_inner(), 42);
        }
        #[test]
        fn test_validated() {
            let id = UserId::new(123);
            let validated = id.validate().unwrap();
            assert_eq!(validated.get(), 123);
        }
    }

    Deep Comparison

    Phantom Type Basics

    See example files for comparison.

    Exercises

  • Create ProductId<State> and OrderId<State> phantom types and write a function create_order(user: UserId<Validated>, product: ProductId<Validated>) -> OrderId<Unvalidated>.
  • Add a Sanitized marker and a sanitize(raw: Tagged<String, Raw>) -> Tagged<String, Sanitized> function that strips HTML tags. Ensure render only accepts Tagged<String, Sanitized>.
  • Implement a TypeMap<Tag, V> that stores values keyed by phantom-tagged keys, preventing retrieval with the wrong tag type.
  • Open Source Repos