Phantom Types

Zero-runtime-cost type information

advanced
phantomzero-costmarker
🎮 Interactive Playground

What are Phantom Types?

Phantom types are type parameters that appear in a type's definition but never appear in its fields at runtime. They exist only at compile time to enforce additional type safety guarantees, then disappear completely during compilation - leaving zero runtime overhead.

The key insight: If you can encode invariants in the type system that vanish at runtime, you get compile-time safety with zero-cost abstractions.

Core Principles

  1. Type-Level Information: Store metadata in types, not data
  2. Zero Runtime Cost: Phantom types compile to nothing (ZST optimization)
  3. Compile-Time Guarantees: Type errors prevent runtime bugs
  4. PhantomData Marker: std::marker::PhantomData tracks phantom types
  5. Variance Control: Phantom types affect subtyping relationships

Why Phantom Types?

// WITHOUT phantom types (runtime checks or confusion)
struct Distance {
    value: f64,
    unit: Unit, // Runtime enum or string - overhead!
}

enum Unit {
    Meters,
    Kilometers,
    Miles,
}

fn calculate_speed(distance: Distance, time: f64) -> f64 {
    // Hope the distance is in the right units!
    // Runtime unit checking needed
    match distance.unit {
        Unit::Meters => distance.value / time,
        Unit::Kilometers => (distance.value * 1000.0) / time,
        Unit::Miles => (distance.value * 1609.34) / time,
    }
}

// WITH phantom types (compile-time safety, zero cost)
use std::marker::PhantomData;

struct Meters;
struct Kilometers;

struct Distance<Unit> {
    value: f64,
    _unit: PhantomData<Unit>, // Zero size at runtime!
}

fn calculate_speed(distance: Distance<Meters>, time: f64) -> f64 {
    distance.value / time // Type system guarantees units are correct!
}

// Won't compile - type mismatch caught at compile time:
// let km_distance = Distance::<Kilometers> { value: 5.0, _unit: PhantomData };
// calculate_speed(km_distance, 10.0); // ERROR: expected Distance<Meters>
The benefit: No runtime unit storage, no runtime checks, impossible to pass wrong units.

---

Real-World Example 1: Units of Measurement (Scientific Computing)

The Problem

Scientific code with mixed units causes catastrophic bugs. NASA's Mars Climate Orbiter crashed because software mixed metric and imperial units - a $327 million mistake that phantom types prevent.

The Solution

use std::marker::PhantomData;
use std::ops::{Add, Sub, Mul, Div};

// Unit markers (zero-sized types)
struct Meters;
struct Kilometers;
struct Miles;
struct Feet;

// Generic measurement with phantom type parameter
#[derive(Debug, Clone, Copy)]
struct Distance<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

impl<Unit> Distance<Unit> {
    fn new(value: f64) -> Self {
        Distance {
            value,
            _unit: PhantomData,
        }
    }
    
    fn value(&self) -> f64 {
        self.value
    }
}

// Type-safe conversions between units
impl Distance<Meters> {
    fn to_kilometers(self) -> Distance<Kilometers> {
        Distance::new(self.value / 1000.0)
    }
    
    fn to_miles(self) -> Distance<Miles> {
        Distance::new(self.value / 1609.34)
    }
    
    fn to_feet(self) -> Distance<Feet> {
        Distance::new(self.value * 3.28084)
    }
}

impl Distance<Kilometers> {
    fn to_meters(self) -> Distance<Meters> {
        Distance::new(self.value * 1000.0)
    }
    
    fn to_miles(self) -> Distance<Miles> {
        Distance::new(self.value * 0.621371)
    }
}

impl Distance<Miles> {
    fn to_meters(self) -> Distance<Meters> {
        Distance::new(self.value * 1609.34)
    }
    
    fn to_kilometers(self) -> Distance<Kilometers> {
        Distance::new(self.value * 1.60934)
    }
}

// Can only add/subtract same units
impl<Unit> Add for Distance<Unit> {
    type Output = Distance<Unit>;
    
    fn add(self, rhs: Self) -> Self::Output {
        Distance::new(self.value + rhs.value)
    }
}

impl<Unit> Sub for Distance<Unit> {
    type Output = Distance<Unit>;
    
    fn sub(self, rhs: Self) -> Self::Output {
        Distance::new(self.value - rhs.value)
    }
}

// Can multiply/divide by scalars
impl<Unit> Mul<f64> for Distance<Unit> {
    type Output = Distance<Unit>;
    
    fn mul(self, rhs: f64) -> Self::Output {
        Distance::new(self.value * rhs)
    }
}

impl<Unit> Div<f64> for Distance<Unit> {
    type Output = Distance<Unit>;
    
    fn div(self, rhs: f64) -> Self::Output {
        Distance::new(self.value / rhs)
    }
}

// Temperature with similar pattern
struct Celsius;
struct Fahrenheit;
struct Kelvin;

#[derive(Debug, Clone, Copy)]
struct Temperature<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

impl<Unit> Temperature<Unit> {
    fn new(value: f64) -> Self {
        Temperature {
            value,
            _unit: PhantomData,
        }
    }
}

impl Temperature<Celsius> {
    fn to_fahrenheit(self) -> Temperature<Fahrenheit> {
        Temperature::new(self.value * 9.0 / 5.0 + 32.0)
    }
    
    fn to_kelvin(self) -> Temperature<Kelvin> {
        Temperature::new(self.value + 273.15)
    }
}

impl Temperature<Fahrenheit> {
    fn to_celsius(self) -> Temperature<Celsius> {
        Temperature::new((self.value - 32.0) * 5.0 / 9.0)
    }
}

// Usage in scientific calculations
fn calculate_orbital_speed(altitude: Distance<Kilometers>) -> Distance<Kilometers> {
    let earth_radius = Distance::<Kilometers>::new(6371.0);
    let orbital_radius = earth_radius + altitude;
    let gravitational_parameter = 398600.4418; // km³/s²
    
    Distance::new((gravitational_parameter / orbital_radius.value()).sqrt())
}

fn nasa_example() {
    let spacecraft_altitude = Distance::<Miles>::new(250.0);
    
    // Type system enforces correct conversion!
    let altitude_km = spacecraft_altitude.to_kilometers();
    let orbital_speed = calculate_orbital_speed(altitude_km);
    
    println!("Orbital speed: {:.2} km/s", orbital_speed.value());
    
    // This won't compile - prevents Mars Orbiter disaster:
    // let orbital_speed = calculate_orbital_speed(spacecraft_altitude);
    // ERROR: expected Distance<Kilometers>, found Distance<Miles>
    
    // Temperature calculations
    let room_temp = Temperature::<Fahrenheit>::new(72.0);
    let celsius = room_temp.to_celsius();
    println!("Room temperature: {:.1}°C", celsius.value);
    
    // Can't accidentally mix temperatures:
    // let result = room_temp + celsius; 
    // ERROR: no implementation for `Temperature<Fahrenheit> + Temperature<Celsius>`
}
Zero-cost proof:
// Memory layout proof
fn memory_layout_proof() {
    use std::mem::{size_of, align_of};
    
    // Phantom types add ZERO runtime overhead
    assert_eq!(size_of::<Distance<Meters>>(), size_of::<f64>());
    assert_eq!(size_of::<Distance<Kilometers>>(), size_of::<f64>());
    assert_eq!(size_of::<Temperature<Celsius>>(), size_of::<f64>());
    
    // PhantomData itself is zero-sized
    assert_eq!(size_of::<PhantomData<Meters>>(), 0);
    assert_eq!(align_of::<PhantomData<Meters>>(), 1);
    
    println!("Size of Distance<Meters>: {} bytes", size_of::<Distance<Meters>>());
    println!("Size of f64: {} bytes", size_of::<f64>());
    println!("Phantom type overhead: ZERO bytes!");
}

---

Real-World Example 2: Type-Safe Database IDs (Backend Services)

The Problem

In large applications with many database tables, passing wrong ID types causes subtle bugs. Passing a UserId where a PostId is expected leads to cryptic "not found" errors or worse - data corruption.

The Solution

use std::marker::PhantomData;
use std::fmt;

// Entity markers
struct User;
struct Post;
struct Comment;
struct Organization;
struct Team;

// Generic ID with phantom type
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct Id<Entity> {
    value: u64,
    _entity: PhantomData<Entity>,
}

impl<Entity> Id<Entity> {
    fn new(value: u64) -> Self {
        Id {
            value,
            _entity: PhantomData,
        }
    }
    
    fn value(&self) -> u64 {
        self.value
    }
}

impl<Entity> fmt::Display for Id<Entity> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.value)
    }
}

// Database models with type-safe IDs
struct UserModel {
    id: Id<User>,
    name: String,
    org_id: Id<Organization>,
}

struct PostModel {
    id: Id<Post>,
    author_id: Id<User>,
    content: String,
}

struct CommentModel {
    id: Id<Comment>,
    post_id: Id<Post>,
    author_id: Id<User>,
    text: String,
}

// Type-safe database operations
trait Repository<Entity> {
    type Model;
    
    fn find(&self, id: Id<Entity>) -> Option<Self::Model>;
    fn delete(&mut self, id: Id<Entity>) -> bool;
}

struct UserRepository;

impl Repository<User> for UserRepository {
    type Model = UserModel;
    
    fn find(&self, id: Id<User>) -> Option<UserModel> {
        // Database query using id.value()
        println!("SELECT * FROM users WHERE id = {}", id.value());
        None // Simplified
    }
    
    fn delete(&mut self, id: Id<User>) -> bool {
        println!("DELETE FROM users WHERE id = {}", id.value());
        true
    }
}

struct PostRepository;

impl Repository<Post> for PostRepository {
    type Model = PostModel;
    
    fn find(&self, id: Id<Post>) -> Option<PostModel> {
        println!("SELECT * FROM posts WHERE id = {}", id.value());
        None
    }
    
    fn delete(&mut self, id: Id<Post>) -> bool {
        println!("DELETE FROM posts WHERE id = {}", id.value());
        true
    }
}

// Type-safe foreign key relationships
struct PostService {
    post_repo: PostRepository,
    user_repo: UserRepository,
}

impl PostService {
    fn get_post_with_author(&self, post_id: Id<Post>) -> Option<(PostModel, UserModel)> {
        let post = self.post_repo.find(post_id)?;
        let author = self.user_repo.find(post.author_id)?;
        Some((post, author))
    }
    
    fn delete_user_posts(&mut self, user_id: Id<User>) {
        // Type system ensures we use correct ID types
        println!("Finding posts by user {}", user_id);
        // self.post_repo.delete(user_id); // ERROR: expected Id<Post>, found Id<User>
    }
    
    fn transfer_post_ownership(&mut self, post_id: Id<Post>, new_owner: Id<User>) {
        println!("Transferring post {} to user {}", post_id, new_owner);
        // Type safety prevents mixing up parameters!
    }
}

// Generic query builder with type-safe IDs
struct QueryBuilder<Entity> {
    table: &'static str,
    conditions: Vec<String>,
    _entity: PhantomData<Entity>,
}

impl<Entity> QueryBuilder<Entity> {
    fn new(table: &'static str) -> Self {
        QueryBuilder {
            table,
            conditions: Vec::new(),
            _entity: PhantomData,
        }
    }
    
    fn where_id(mut self, id: Id<Entity>) -> Self {
        self.conditions.push(format!("id = {}", id.value()));
        self
    }
    
    fn build(self) -> String {
        let mut query = format!("SELECT * FROM {}", self.table);
        if !self.conditions.is_empty() {
            query.push_str(" WHERE ");
            query.push_str(&self.conditions.join(" AND "));
        }
        query
    }
}

fn database_example() {
    let user_id = Id::<User>::new(42);
    let post_id = Id::<Post>::new(123);
    let comment_id = Id::<Comment>::new(456);
    
    let user_repo = UserRepository;
    let post_repo = PostRepository;
    
    // Type-safe queries
    user_repo.find(user_id);
    post_repo.find(post_id);
    
    // Won't compile - prevents ID mixups:
    // user_repo.find(post_id); // ERROR: expected Id<User>, found Id<Post>
    // post_repo.delete(user_id); // ERROR: expected Id<Post>, found Id<User>
    
    // Type-safe query builder
    let query = QueryBuilder::<User>::new("users")
        .where_id(user_id)
        .build();
    println!("Query: {}", query);
    
    // Won't compile:
    // let bad_query = QueryBuilder::<User>::new("users")
    //     .where_id(post_id) // ERROR: expected Id<User>, found Id<Post>
    //     .build();
    
    // Foreign key safety
    let service = PostService { post_repo, user_repo };
    service.get_post_with_author(post_id);
    
    // Memory efficiency proof
    println!("\nMemory efficiency:");
    println!("Size of Id<User>: {} bytes", std::mem::size_of::<Id<User>>());
    println!("Size of Id<Post>: {} bytes", std::mem::size_of::<Id<Post>>());
    println!("Size of u64: {} bytes", std::mem::size_of::<u64>());
    println!("Phantom type overhead: ZERO bytes!");
}
Real-world impact: This pattern prevents entire classes of bugs in large codebases. At scale, ID mixups can cause data leaks or corruption. Type-safe IDs make these bugs impossible.

---

Real-World Example 3: Validated Types (Security & Parsing)

The Problem

Security vulnerabilities often come from using unvalidated data. Email injection, SQL injection, XSS - all happen when unvalidated strings are used in security-sensitive contexts.

The Solution: Parse, Don't Validate

use std::marker::PhantomData;

// Validation state markers
struct Validated;
struct Unvalidated;

// Generic email with validation state
struct Email<State = Unvalidated> {
    address: String,
    _state: PhantomData<State>,
}

#[derive(Debug)]
enum ValidationError {
    MissingAtSign,
    EmptyLocalPart,
    EmptyDomain,
    InvalidCharacters,
}

impl Email<Unvalidated> {
    fn new(address: String) -> Self {
        Email {
            address,
            _state: PhantomData,
        }
    }
    
    fn validate(self) -> Result<Email<Validated>, ValidationError> {
        // Validation logic
        if !self.address.contains('@') {
            return Err(ValidationError::MissingAtSign);
        }
        
        let parts: Vec<&str> = self.address.split('@').collect();
        if parts.len() != 2 {
            return Err(ValidationError::MissingAtSign);
        }
        
        if parts[0].is_empty() {
            return Err(ValidationError::EmptyLocalPart);
        }
        
        if parts[1].is_empty() {
            return Err(ValidationError::EmptyDomain);
        }
        
        // More validation...
        
        Ok(Email {
            address: self.address,
            _state: PhantomData,
        })
    }
}

impl Email<Validated> {
    fn address(&self) -> &str {
        &self.address
    }
    
    fn domain(&self) -> &str {
        self.address.split('@').nth(1).unwrap() // Safe - validation guarantees this
    }
}

// Only validated emails can be sent
fn send_email(email: Email<Validated>, body: &str) {
    println!("Sending email to: {}", email.address());
    println!("Body: {}", body);
}

// Sanitized HTML
struct Sanitized;
struct Unsanitized;

struct HtmlString<State = Unsanitized> {
    content: String,
    _state: PhantomData<State>,
}

impl HtmlString<Unsanitized> {
    fn new(content: String) -> Self {
        HtmlString {
            content,
            _state: PhantomData,
        }
    }
    
    fn sanitize(self) -> HtmlString<Sanitized> {
        let sanitized = self.content
            .replace('<', "&lt;")
            .replace('>', "&gt;")
            .replace('&', "&amp;")
            .replace('"', "&quot;")
            .replace('\'', "&#x27;");
        
        HtmlString {
            content: sanitized,
            _state: PhantomData,
        }
    }
}

impl HtmlString<Sanitized> {
    fn content(&self) -> &str {
        &self.content
    }
}

// Only sanitized HTML can be rendered
fn render_html(html: HtmlString<Sanitized>) -> String {
    format!("<div>{}</div>", html.content())
}

// SQL safe strings
struct SqlSafe;
struct SqlUnsafe;

struct SqlString<State = SqlUnsafe> {
    value: String,
    _state: PhantomData<State>,
}

impl SqlString<SqlUnsafe> {
    fn new(value: String) -> Self {
        SqlString {
            value,
            _state: PhantomData,
        }
    }
    
    fn escape(self) -> SqlString<SqlSafe> {
        let escaped = self.value
            .replace('\\', "\\\\")
            .replace('\'', "''")
            .replace('"', "\\\"")
            .replace('\0', "\\0");
        
        SqlString {
            value: escaped,
            _state: PhantomData,
        }
    }
}

impl SqlString<SqlSafe> {
    fn value(&self) -> &str {
        &self.value
    }
}

// Only escaped strings can be used in queries
fn execute_query(table: &str, value: SqlString<SqlSafe>) {
    let query = format!("SELECT * FROM {} WHERE name = '{}'", table, value.value());
    println!("Executing: {}", query);
}

// URL validation
struct ValidUrl;
struct InvalidUrl;

struct Url<State = InvalidUrl> {
    url: String,
    _state: PhantomData<State>,
}

impl Url<InvalidUrl> {
    fn new(url: String) -> Self {
        Url {
            url,
            _state: PhantomData,
        }
    }
    
    fn validate(self) -> Result<Url<ValidUrl>, String> {
        if self.url.starts_with("http://") || self.url.starts_with("https://") {
            Ok(Url {
                url: self.url,
                _state: PhantomData,
            })
        } else {
            Err("URL must start with http:// or https://".to_string())
        }
    }
}

impl Url<ValidUrl> {
    fn as_str(&self) -> &str {
        &self.url
    }
}

fn fetch_url(url: Url<ValidUrl>) -> Result<String, std::io::Error> {
    println!("Fetching: {}", url.as_str());
    Ok("Response body".to_string())
}

fn security_example() {
    // Email validation
    let raw_email = Email::new("user@example.com".to_string());
    
    match raw_email.validate() {
        Ok(valid_email) => {
            send_email(valid_email, "Hello!");
            // Won't compile with unvalidated email:
            // let bad_email = Email::new("notanemail".to_string());
            // send_email(bad_email, "Hi"); // ERROR: expected Email<Validated>
        }
        Err(e) => println!("Invalid email: {:?}", e),
    }
    
    // HTML sanitization prevents XSS
    let user_input = HtmlString::new("<script>alert('XSS')</script>".to_string());
    let safe_html = user_input.sanitize();
    let rendered = render_html(safe_html);
    println!("Safe HTML: {}", rendered);
    
    // Won't compile - prevents XSS:
    // let unsafe_html = HtmlString::new("<script>...".to_string());
    // render_html(unsafe_html); // ERROR: expected HtmlString<Sanitized>
    
    // SQL injection prevention
    let user_input = SqlString::new("'; DROP TABLE users; --".to_string());
    let safe_sql = user_input.escape();
    execute_query("users", safe_sql);
    
    // Won't compile - prevents SQL injection:
    // let unsafe_sql = SqlString::new("malicious".to_string());
    // execute_query("users", unsafe_sql); // ERROR: expected SqlString<SqlSafe>
    
    // URL validation
    let url = Url::new("https://example.com".to_string());
    if let Ok(valid_url) = url.validate() {
        fetch_url(valid_url).ok();
    }
}
Security impact: Phantom types implement the "parse, don't validate" principle. Once data is validated, the type system guarantees it stays validated. No re-validation needed, no validation bypass bugs possible.

---

Real-World Example 4: Protocol Versioning (Network Programming)

The Problem

APIs evolve over time. Supporting multiple protocol versions while preventing version mixups requires careful management. Accidentally using v1 parsing on v2 data causes bugs.

The Solution

use std::marker::PhantomData;

// Protocol version markers
struct V1;
struct V2;
struct V3;

// Generic protocol message
#[derive(Debug)]
struct Message<Version> {
    payload: Vec<u8>,
    _version: PhantomData<Version>,
}

impl<V> Message<V> {
    fn payload(&self) -> &[u8] {
        &self.payload
    }
}

// Version-specific parsing
impl Message<V1> {
    fn parse(data: Vec<u8>) -> Result<Self, String> {
        if data.len() < 4 {
            return Err("V1 message too short".to_string());
        }
        Ok(Message {
            payload: data,
            _version: PhantomData,
        })
    }
    
    fn get_field(&self) -> u32 {
        // V1-specific field layout
        u32::from_be_bytes([self.payload[0], self.payload[1], 
                           self.payload[2], self.payload[3]])
    }
    
    // Migration to V2
    fn upgrade(self) -> Message<V2> {
        let mut new_payload = vec![0x02]; // V2 marker
        new_payload.extend_from_slice(&self.payload);
        
        Message {
            payload: new_payload,
            _version: PhantomData,
        }
    }
}

impl Message<V2> {
    fn parse(data: Vec<u8>) -> Result<Self, String> {
        if data.len() < 5 || data[0] != 0x02 {
            return Err("Invalid V2 message".to_string());
        }
        Ok(Message {
            payload: data,
            _version: PhantomData,
        })
    }
    
    fn get_field(&self) -> u32 {
        // V2-specific field layout (offset by version byte)
        u32::from_be_bytes([self.payload[1], self.payload[2], 
                           self.payload[3], self.payload[4]])
    }
    
    fn get_extended_field(&self) -> Option<u16> {
        // V2 adds new field
        if self.payload.len() >= 7 {
            Some(u16::from_be_bytes([self.payload[5], self.payload[6]]))
        } else {
            None
        }
    }
    
    fn upgrade(self) -> Message<V3> {
        let mut new_payload = vec![0x03]; // V3 marker
        new_payload.extend_from_slice(&self.payload[1..]); // Skip old version marker
        
        Message {
            payload: new_payload,
            _version: PhantomData,
        }
    }
}

impl Message<V3> {
    fn parse(data: Vec<u8>) -> Result<Self, String> {
        if data.len() < 8 || data[0] != 0x03 {
            return Err("Invalid V3 message".to_string());
        }
        Ok(Message {
            payload: data,
            _version: PhantomData,
        })
    }
    
    fn get_field(&self) -> u32 {
        u32::from_be_bytes([self.payload[1], self.payload[2], 
                           self.payload[3], self.payload[4]])
    }
    
    fn get_extended_field(&self) -> u16 {
        u16::from_be_bytes([self.payload[5], self.payload[6]])
    }
    
    fn get_timestamp(&self) -> u64 {
        // V3 adds timestamp
        u64::from_be_bytes([
            self.payload[7], self.payload[8], self.payload[9], self.payload[10],
            self.payload[11], self.payload[12], self.payload[13], self.payload[14],
        ])
    }
}

// Version-specific handlers
trait MessageHandler<Version> {
    fn handle(&self, message: Message<Version>);
}

struct V1Handler;
impl MessageHandler<V1> for V1Handler {
    fn handle(&self, message: Message<V1>) {
        println!("V1 Handler - Field: {}", message.get_field());
    }
}

struct V2Handler;
impl MessageHandler<V2> for V2Handler {
    fn handle(&self, message: Message<V2>) {
        println!("V2 Handler - Field: {}, Extended: {:?}", 
                 message.get_field(), message.get_extended_field());
    }
}

struct V3Handler;
impl MessageHandler<V3> for V3Handler {
    fn handle(&self, message: Message<V3>) {
        println!("V3 Handler - Field: {}, Extended: {}, Timestamp: {}", 
                 message.get_field(), message.get_extended_field(), message.get_timestamp());
    }
}

// HTTP request versioning
struct HttpRequest<Version> {
    path: String,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
    _version: PhantomData<Version>,
}

impl HttpRequest<V1> {
    fn new(path: String) -> Self {
        HttpRequest {
            path,
            headers: Vec::new(),
            body: Vec::new(),
            _version: PhantomData,
        }
    }
    
    fn add_header(mut self, key: String, value: String) -> Self {
        self.headers.push((key, value));
        self
    }
}

impl HttpRequest<V2> {
    fn new(path: String) -> Self {
        HttpRequest {
            path,
            headers: vec![("X-API-Version".to_string(), "2".to_string())],
            body: Vec::new(),
            _version: PhantomData,
        }
    }
    
    fn add_header(mut self, key: String, value: String) -> Self {
        self.headers.push((key, value));
        self
    }
    
    fn with_json_body(mut self, json: Vec<u8>) -> Self {
        self.body = json;
        self.headers.push(("Content-Type".to_string(), "application/json".to_string()));
        self
    }
}

// Version-specific endpoints
fn handle_v1_request(req: HttpRequest<V1>) {
    println!("Handling V1 request to: {}", req.path);
    println!("Headers: {} (V1 doesn't require version header)", req.headers.len());
}

fn handle_v2_request(req: HttpRequest<V2>) {
    println!("Handling V2 request to: {}", req.path);
    println!("Headers: {} (includes version header)", req.headers.len());
    println!("Body size: {} bytes", req.body.len());
}

fn protocol_example() {
    // Parse different versions
    let v1_data = vec![0x00, 0x00, 0x00, 0x42];
    let v1_msg = Message::<V1>::parse(v1_data).unwrap();
    println!("V1 field: {}", v1_msg.get_field());
    
    // Type-safe upgrade path
    let v2_msg = v1_msg.upgrade();
    println!("Upgraded to V2 - Extended field: {:?}", v2_msg.get_extended_field());
    
    let v3_msg = v2_msg.upgrade();
    println!("Upgraded to V3 - Timestamp: {}", v3_msg.get_timestamp());
    
    // Won't compile - prevents version confusion:
    // let v2_handler = V2Handler;
    // let v1_msg = Message::<V1>::parse(vec![1, 2, 3, 4]).unwrap();
    // v2_handler.handle(v1_msg); // ERROR: expected Message<V2>, found Message<V1>
    
    // HTTP versioning
    let v1_req = HttpRequest::<V1>::new("/users".to_string())
        .add_header("Accept".to_string(), "application/json".to_string());
    handle_v1_request(v1_req);
    
    let v2_req = HttpRequest::<V2>::new("/users".to_string())
        .with_json_body(b"{}".to_vec());
    handle_v2_request(v2_req);
    
    // Won't compile:
    // handle_v2_request(v1_req); // ERROR: expected HttpRequest<V2>
}

---

Real-World Example 5: Ownership Tracking (Systems Programming)

The Problem

In systems programming, tracking whether a buffer is owned or borrowed is critical. Freeing borrowed memory causes crashes. Phantom types can track this at compile time.

The Solution

use std::marker::PhantomData;
use std::ptr;

// Ownership state markers
struct Owned;
struct Borrowed;

// Buffer with ownership tracking
struct Buffer<Ownership> {
    data: *mut u8,
    len: usize,
    capacity: usize,
    _ownership: PhantomData<Ownership>,
}

impl Buffer<Owned> {
    fn new(capacity: usize) -> Self {
        let layout = std::alloc::Layout::from_size_align(capacity, 1).unwrap();
        let data = unsafe { std::alloc::alloc(layout) };
        
        Buffer {
            data,
            len: 0,
            capacity,
            _ownership: PhantomData,
        }
    }
    
    fn from_vec(mut vec: Vec<u8>) -> Self {
        let data = vec.as_mut_ptr();
        let len = vec.len();
        let capacity = vec.capacity();
        std::mem::forget(vec); // Don't drop the Vec
        
        Buffer {
            data,
            len,
            capacity,
            _ownership: PhantomData,
        }
    }
    
    fn as_borrowed(&self) -> Buffer<Borrowed> {
        Buffer {
            data: self.data,
            len: self.len,
            capacity: self.capacity,
            _ownership: PhantomData,
        }
    }
    
    fn write(&mut self, byte: u8) {
        if self.len < self.capacity {
            unsafe {
                ptr::write(self.data.add(self.len), byte);
            }
            self.len += 1;
        }
    }
}

impl<O> Buffer<O> {
    fn read(&self, index: usize) -> Option<u8> {
        if index < self.len {
            unsafe { Some(ptr::read(self.data.add(index))) }
        } else {
            None
        }
    }
    
    fn len(&self) -> usize {
        self.len
    }
    
    fn as_slice(&self) -> &[u8] {
        unsafe { std::slice::from_raw_parts(self.data, self.len) }
    }
}

// Only owned buffers can be freed
impl Drop for Buffer<Owned> {
    fn drop(&mut self) {
        if !self.data.is_null() && self.capacity > 0 {
            unsafe {
                let layout = std::alloc::Layout::from_size_align(self.capacity, 1).unwrap();
                std::alloc::dealloc(self.data, layout);
            }
        }
    }
}

// Borrowed buffers don't free memory - no Drop impl!

// File handle with ownership
struct FileDescriptor<Ownership> {
    fd: i32,
    _ownership: PhantomData<Ownership>,
}

impl FileDescriptor<Owned> {
    fn open(path: &str) -> std::io::Result<Self> {
        // Simplified - would actually open file
        println!("Opening file: {}", path);
        Ok(FileDescriptor {
            fd: 42, // Mock file descriptor
            _ownership: PhantomData,
        })
    }
    
    fn as_borrowed(&self) -> FileDescriptor<Borrowed> {
        FileDescriptor {
            fd: self.fd,
            _ownership: PhantomData,
        }
    }
}

impl<O> FileDescriptor<O> {
    fn read(&self, buf: &mut [u8]) -> std::io::Result<usize> {
        println!("Reading from fd {}", self.fd);
        Ok(0)
    }
    
    fn write(&self, buf: &[u8]) -> std::io::Result<usize> {
        println!("Writing {} bytes to fd {}", buf.len(), self.fd);
        Ok(buf.len())
    }
}

// Only owned file descriptors are closed
impl Drop for FileDescriptor<Owned> {
    fn drop(&mut self) {
        println!("Closing file descriptor: {}", self.fd);
        // Would actually close fd here
    }
}

// Resource pool with ownership tracking
struct ResourceHandle<Resource, Ownership> {
    id: usize,
    _resource: PhantomData<Resource>,
    _ownership: PhantomData<Ownership>,
}

struct Database;
struct Connection;

struct ConnectionPool {
    connections: Vec<ResourceHandle<Connection, Owned>>,
}

impl ConnectionPool {
    fn new(size: usize) -> Self {
        let connections = (0..size)
            .map(|id| ResourceHandle {
                id,
                _resource: PhantomData,
                _ownership: PhantomData,
            })
            .collect();
        
        ConnectionPool { connections }
    }
    
    fn acquire(&mut self) -> Option<ResourceHandle<Connection, Borrowed>> {
        self.connections.pop().map(|owned| ResourceHandle {
            id: owned.id,
            _resource: PhantomData,
            _ownership: PhantomData,
        })
    }
    
    fn release(&mut self, borrowed: ResourceHandle<Connection, Borrowed>) {
        // Convert borrowed back to owned for pool storage
        self.connections.push(ResourceHandle {
            id: borrowed.id,
            _resource: PhantomData,
            _ownership: PhantomData,
        });
    }
}

fn ownership_example() {
    // Owned buffer - will be freed
    let mut owned_buffer = Buffer::<Owned>::new(1024);
    owned_buffer.write(42);
    owned_buffer.write(43);
    
    println!("Owned buffer length: {}", owned_buffer.len());
    
    // Borrowed buffer - won't be freed
    {
        let borrowed = owned_buffer.as_borrowed();
        println!("Borrowed buffer length: {}", borrowed.len());
        println!("Read from borrowed: {:?}", borrowed.read(0));
        
        // Can't write to borrowed buffer (no write method)
        // borrowed.write(44); // ERROR: method not found
    } // borrowed dropped here - doesn't free memory
    
    // Original owned buffer still valid
    println!("Owned buffer still valid: {:?}", owned_buffer.read(0));
    
    // File descriptor ownership
    let owned_fd = FileDescriptor::<Owned>::open("/tmp/test.txt").unwrap();
    {
        let borrowed_fd = owned_fd.as_borrowed();
        borrowed_fd.write(b"Hello").ok();
    } // borrowed_fd dropped - doesn't close file
    
    owned_fd.write(b"World").ok();
    // owned_fd dropped here - closes file
    
    // Connection pool
    let mut pool = ConnectionPool::new(5);
    let conn1 = pool.acquire().unwrap();
    let conn2 = pool.acquire().unwrap();
    
    println!("Acquired connection: {}", conn1.id);
    
    // Return to pool
    pool.release(conn1);
    pool.release(conn2);
}

---

Deep Dive: How Phantom Types Work

PhantomData Definition

use std::marker::PhantomData;

// PhantomData is defined in std as:
pub struct PhantomData<T: ?Sized>;

// It's a zero-sized type (ZST) that acts as if it owns a T
// but doesn't actually store any data

// Compiler treats PhantomData<T> as if the struct owns T
// This affects variance, drop check, and auto traits

Zero-Sized Type (ZST) Optimization

use std::mem::{size_of, align_of};

struct NoPhantom {
    value: u64,
}

struct WithPhantom<T> {
    value: u64,
    _phantom: PhantomData<T>,
}

fn zst_proof() {
    // PhantomData adds ZERO bytes
    assert_eq!(size_of::<PhantomData<String>>(), 0);
    assert_eq!(size_of::<PhantomData<Vec<u8>>>(), 0);
    assert_eq!(size_of::<PhantomData<[u8; 1000]>>(), 0);
    
    // Structs with PhantomData have same size as without
    assert_eq!(size_of::<NoPhantom>(), 8);
    assert_eq!(size_of::<WithPhantom<String>>(), 8);
    assert_eq!(size_of::<WithPhantom<Vec<u8>>>(), 8);
    
    // Alignment is minimal
    assert_eq!(align_of::<PhantomData<String>>(), 1);
    
    println!("PhantomData overhead: ZERO bytes!");
}

// Assembly comparison
fn process_no_phantom(x: NoPhantom) -> u64 {
    x.value * 2
}

fn process_with_phantom(x: WithPhantom<String>) -> u64 {
    x.value * 2
}

// Both functions compile to IDENTICAL assembly:
// mov rax, rdi
// add rax, rdi
// ret

Variance and PhantomData

Variance controls how subtyping works with generics. PhantomData affects variance.

use std::marker::PhantomData;

// Covariant: If Dog <: Animal, then Container<Dog> <: Container<Animal>
struct Covariant<T> {
    _marker: PhantomData<T>,
}

// Invariant: No subtyping relationship regardless of T's relationship
struct Invariant<T> {
    _marker: PhantomData<fn(T) -> T>,
}

// Contravariant: If Dog <: Animal, then Container<Animal> <: Container<Dog>
struct Contravariant<T> {
    _marker: PhantomData<fn(T)>,
}

// Practical example: lifetime variance
struct LifetimeCovariant<'a> {
    // Covariant in 'a
    _marker: PhantomData<&'a ()>,
}

struct LifetimeInvariant<'a> {
    // Invariant in 'a
    _marker: PhantomData<&'a mut ()>,
}

fn variance_example() {
    // Covariant: 'static can be used where 'a is expected
    let static_ref: &'static str = "hello";
    let covariant: LifetimeCovariant<'static> = LifetimeCovariant {
        _marker: PhantomData,
    };
    
    // This works because LifetimeCovariant is covariant
    let _shorter: LifetimeCovariant<'_> = covariant;
    
    // Invariant lifetime prevents similar coercion
    let invariant: LifetimeInvariant<'static> = LifetimeInvariant {
        _marker: PhantomData,
    };
    
    // This doesn't work - invariant prevents coercion
    // let _shorter: LifetimeInvariant<'_> = invariant; // ERROR
}

Drop Check Interaction

PhantomData affects drop check - Rust's mechanism to prevent use-after-free.

use std::marker::PhantomData;

// Without PhantomData - UNSOUND
struct WithoutPhantom<T> {
    ptr: *const T,
}

impl<T> Drop for WithoutPhantom<T> {
    fn drop(&mut self) {
        // DANGER: T might be dropped before this runs!
        // unsafe { ptr::read(self.ptr); } // Potential use-after-free
    }
}

// With PhantomData - SOUND
struct WithPhantom<T> {
    ptr: *const T,
    _marker: PhantomData<T>, // Tells drop checker we "own" T
}

impl<T> Drop for WithPhantom<T> {
    fn drop(&mut self) {
        // Safe: drop checker ensures T outlives self
        // unsafe { ptr::read(self.ptr); }
    }
}

// Real example: Vec-like structure
struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    cap: usize,
    _marker: PhantomData<T>, // Critical for soundness!
}

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        // PhantomData ensures this is sound
        unsafe {
            // Drop all elements
            for i in 0..self.len {
                ptr::drop_in_place(self.ptr.add(i));
            }
            
            // Deallocate buffer
            if self.cap > 0 {
                let layout = std::alloc::Layout::array::<T>(self.cap).unwrap();
                std::alloc::dealloc(self.ptr as *mut u8, layout);
            }
        }
    }
}

Type-Level Programming

Phantom types enable type-level computation.

use std::marker::PhantomData;

// Type-level booleans
struct True;
struct False;

// Type-level logic
trait And<Rhs> {
    type Output;
}

impl And<True> for True {
    type Output = True;
}

impl And<False> for True {
    type Output = False;
}

impl And<True> for False {
    type Output = False;
}

impl And<False> for False {
    type Output = False;
}

// Type-level numbers (Peano numbers)
struct Zero;
struct Succ<N>(PhantomData<N>);

type One = Succ<Zero>;
type Two = Succ<One>;
type Three = Succ<Two>;

// Type-level addition
trait Add<Rhs> {
    type Output;
}

impl<N> Add<Zero> for N {
    type Output = N;
}

impl<N, M> Add<Succ<M>> for N
where
    N: Add<M>,
{
    type Output = Succ<<N as Add<M>>::Output>;
}

// Array with type-level length
struct TypedArray<T, N> {
    data: Vec<T>,
    _len: PhantomData<N>,
}

impl<T> TypedArray<T, Zero> {
    fn new() -> Self {
        TypedArray {
            data: Vec::new(),
            _len: PhantomData,
        }
    }
}

impl<T, N> TypedArray<T, N> {
    fn push(self, item: T) -> TypedArray<T, Succ<N>> {
        let mut data = self.data;
        data.push(item);
        TypedArray {
            data,
            _len: PhantomData,
        }
    }
}

fn type_level_programming() {
    // Array with length encoded in type
    let arr = TypedArray::<i32, Zero>::new()
        .push(1)  // TypedArray<i32, One>
        .push(2)  // TypedArray<i32, Two>
        .push(3); // TypedArray<i32, Three>
    
    // Type system knows exact length!
    // This enables compile-time bounds checking
}

Compiler's Perspective

// Source code
struct Distance<Unit> {
    value: f64,
    _unit: PhantomData<Unit>,
}

fn double_meters(d: Distance<Meters>) -> Distance<Meters> {
    Distance {
        value: d.value * 2.0,
        _unit: PhantomData,
    }
}

// After monomorphization (conceptually):
// Compiler generates code as if PhantomData doesn't exist

struct Distance_Meters {
    value: f64,
    // _unit field completely erased
}

fn double_meters_monomorphized(d: Distance_Meters) -> Distance_Meters {
    Distance_Meters {
        value: d.value * 2.0,
        // _unit construction erased
    }
}

// Final assembly (x86_64):
// double_meters:
//     movsd   xmm1, qword ptr [rip + .LCPI0_0]  ; Load 2.0
//     mulsd   xmm0, xmm1                          ; Multiply
//     ret
//
// No overhead from phantom type!

---

When to Use Phantom Types

✅ Use When You Need:

  1. Type Safety for Primitive Types
  • Distinguishing IDs, units, currencies
  • Preventing mixups at compile time
  • Example: Id vs Id
  1. State Tracking in Types
  • Validation states, ownership states
  • Parse-don't-validate pattern
  • Example: Email vs Email
  1. Protocol/API Versioning
  • Multiple versions with compile-time safety
  • Type-safe migrations
  • Example: Request vs Request
  1. Zero-Cost Abstractions
  • Type safety without runtime cost
  • When enum or runtime checks are too expensive
  • Example: Units in scientific computing
  1. Variance Control
  • Fine-tuning subtyping relationships
  • Lifetime management in unsafe code
  • Example: Custom smart pointers
  1. Drop Check Safety
  • Unsafe code with raw pointers
  • Ensuring correct drop order
  • Example: Custom collections

❌ Don't Use When You Need:

  1. Runtime Polymorphism
  • Phantom types are compile-time only
  • Use trait objects or enums instead
  • Example: Don't use for plugin systems
  1. Actual Data Storage
  • If you need to inspect the type at runtime
  • Use real fields or enums
  • Example: Don't use for serialization markers
  1. Simple Type Aliases Suffice
  • Phantom types add complexity
  • type UserId = u64 might be enough
  • Only use phantom types when mixups are dangerous
  1. Dynamic Type Checking
  • Runtime type inspection impossible
  • Use TypeId or enums instead
  • Example: Don't use for reflection

---

⚠️ Anti-patterns

1. Using Phantom Types for Runtime Data

// ❌ WRONG: Trying to store runtime information
struct Config<Mode> {
    enabled: bool,
    _mode: PhantomData<Mode>,
}

fn get_mode<M>(config: &Config<M>) -> &str {
    // Can't do this - M doesn't exist at runtime!
    // std::any::type_name::<M>() // Doesn't help - just a string
    "unknown"
}

// ✅ RIGHT: Use enum for runtime information
enum Mode {
    Development,
    Production,
}

struct Config {
    enabled: bool,
    mode: Mode,
}

fn get_mode(config: &Config) -> &str {
    match config.mode {
        Mode::Development => "development",
        Mode::Production => "production",
    }
}

2. Incorrect Variance Annotations

// ❌ WRONG: Incorrect variance can cause unsoundness
struct MutRef<'a, T> {
    reference: &'a mut T,
    // Missing PhantomData - wrong variance!
}

// ✅ RIGHT: Correct variance with PhantomData
struct MutRef<'a, T> {
    reference: &'a mut T,
    _marker: PhantomData<&'a mut T>, // Invariant in T
}

3. Not Using PhantomData Correctly

// ❌ WRONG: Forgetting PhantomData in unsafe code
struct Wrapper<T> {
    ptr: *const T,
    // Missing PhantomData - drop check issues!
}

impl<T> Drop for Wrapper<T> {
    fn drop(&mut self) {
        // UNSOUND: T might be dropped already!
        unsafe { ptr::read(self.ptr); }
    }
}

// ✅ RIGHT: PhantomData for drop check soundness
struct Wrapper<T> {
    ptr: *const T,
    _marker: PhantomData<T>,
}

4. Over-engineering with Phantom Types

// ❌ WRONG: Over-complicated for simple case
struct Username<Validated, MinLength, MaxLength> {
    value: String,
    _v: PhantomData<Validated>,
    _min: PhantomData<MinLength>,
    _max: PhantomData<MaxLength>,
}

// ✅ RIGHT: Simple validation is enough
struct Username {
    value: String,
}

impl Username {
    fn new(value: String) -> Result<Self, ValidationError> {
        if value.len() < 3 || value.len() > 20 {
            return Err(ValidationError::InvalidLength);
        }
        Ok(Username { value })
    }
}

5. Forgetting Drop Check Implications

// ❌ WRONG: Phantom lifetime without considering drop
struct Container<'a, T> {
    ptr: *const T,
    _marker: PhantomData<&'a T>,
}

// Problem: Drop might run after 'a ends
impl<'a, T> Drop for Container<'a, T> {
    fn drop(&mut self) {
        // Might be unsound if we access *ptr here
    }
}

// ✅ RIGHT: Use #[may_dangle] if you understand implications
unsafe impl<#[may_dangle] 'a, T> Drop for Container<'a, T> {
    fn drop(&mut self) {
        // Safe if we promise not to access T through 'a
    }
}

---

Performance Characteristics

Compile-Time Impact

// Phantom types affect compile time, not runtime

// Small impact: Simple phantom type
struct Id<T> {
    value: u64,
    _marker: PhantomData<T>,
}

// Moderate impact: Multiple phantom parameters
struct Cache<K, V, Strategy> {
    data: HashMap<u64, Vec<u8>>,
    _key: PhantomData<K>,
    _value: PhantomData<V>,
    _strategy: PhantomData<Strategy>,
}

// Higher impact: Type-level computation
struct Matrix<Rows, Cols> 
where
    Rows: Add<Rows>,
    Cols: Add<Cols>,
{
    data: Vec<f64>,
    _rows: PhantomData<Rows>,
    _cols: PhantomData<Cols>,
}

// Compilation time increases with:
// 1. Number of monomorphizations (different type parameters)
// 2. Complexity of trait bounds on phantom types
// 3. Type-level computations involving phantom types

Runtime Performance: Zero Cost

use std::marker::PhantomData;
use std::time::Instant;

struct PlainId {
    value: u64,
}

struct TypedId<T> {
    value: u64,
    _marker: PhantomData<T>,
}

struct User;

fn benchmark_plain(iterations: usize) -> u128 {
    let start = Instant::now();
    let mut sum = 0u64;
    
    for i in 0..iterations {
        let id = PlainId { value: i as u64 };
        sum += id.value;
    }
    
    let elapsed = start.elapsed().as_nanos();
    println!("Plain ID sum: {}", sum);
    elapsed
}

fn benchmark_typed(iterations: usize) -> u128 {
    let start = Instant::now();
    let mut sum = 0u64;
    
    for i in 0..iterations {
        let id = TypedId::<User> { value: i as u64, _marker: PhantomData };
        sum += id.value;
    }
    
    let elapsed = start.elapsed().as_nanos();
    println!("Typed ID sum: {}", sum);
    elapsed
}

fn performance_comparison() {
    const ITERATIONS: usize = 10_000_000;
    
    println!("Running {} iterations...", ITERATIONS);
    
    let plain_time = benchmark_plain(ITERATIONS);
    let typed_time = benchmark_typed(ITERATIONS);
    
    println!("\nResults:");
    println!("Plain ID: {} ns", plain_time);
    println!("Typed ID: {} ns", typed_time);
    println!("Difference: {} ns ({:.2}%)", 
             (typed_time as i128 - plain_time as i128).abs(),
             ((typed_time as f64 - plain_time as f64) / plain_time as f64 * 100.0).abs());
    
    // Difference is typically < 1% (within measurement noise)
}

Memory Layout Comparison

use std::mem::{size_of, align_of};

struct NoPhantom {
    id: u64,
    name: String,
}

struct WithOnePhantom<T> {
    id: u64,
    name: String,
    _marker: PhantomData<T>,
}

struct WithManyPhantoms<A, B, C, D> {
    id: u64,
    name: String,
    _a: PhantomData<A>,
    _b: PhantomData<B>,
    _c: PhantomData<C>,
    _d: PhantomData<D>,
}

fn memory_analysis() {
    println!("Memory Layout Analysis:");
    println!("=======================");
    
    // Size comparison
    println!("\nSize (bytes):");
    println!("  NoPhantom:       {}", size_of::<NoPhantom>());
    println!("  WithOnePhantom:  {}", size_of::<WithOnePhantom<String>>());
    println!("  WithManyPhantoms: {}", size_of::<WithManyPhantoms<u8, u16, u32, u64>>());
    
    // All should be identical!
    assert_eq!(size_of::<NoPhantom>(), size_of::<WithOnePhantom<String>>());
    assert_eq!(size_of::<NoPhantom>(), size_of::<WithManyPhantoms<u8, u16, u32, u64>>());
    
    // Alignment comparison
    println!("\nAlignment (bytes):");
    println!("  NoPhantom:       {}", align_of::<NoPhantom>());
    println!("  WithOnePhantom:  {}", align_of::<WithOnePhantom<String>>());
    println!("  WithManyPhantoms: {}", align_of::<WithManyPhantoms<u8, u16, u32, u64>>());
    
    println!("\n✅ Zero overhead confirmed!");
}

Binary Size Impact

// Phantom types can increase binary size through monomorphization

// Small impact: Few instantiations
fn process_user_id(id: Id<User>) -> u64 {
    id.value()
}

// Larger impact: Many instantiations
fn process_id<T>(id: Id<T>) -> u64 {
    id.value()
}

// Each unique T generates a new copy of the function
// process_id::<User>, process_id::<Post>, process_id::<Comment>, etc.

// To minimize binary size:
// 1. Use generic implementations sparingly
// 2. Factor out non-generic code
// 3. Use dynamic dispatch when binary size matters more than performance

fn process_id_optimized<T>(id: Id<T>) -> u64 {
    // Factor out generic-independent code
    process_id_impl(id.value())
}

fn process_id_impl(value: u64) -> u64 {
    // Non-generic implementation (single copy in binary)
    value * 2
}

---

Exercises

Beginner: Type-Safe Currency System

Create a currency system with phantom types that prevents mixing currencies.

// TODO: Implement a currency system with:
// - Currency markers: USD, EUR, GBP, JPY
// - Money<Currency> type with phantom currency
// - Can add/subtract same currency
// - Cannot add different currencies (compile error)
// - Conversion methods between currencies
// - Display formatting with currency symbols

// Starter code:
use std::marker::PhantomData;

struct USD;
struct EUR;
struct GBP;

struct Money<Currency> {
    // Your code here
}

// Implement methods:
// - new(amount: f64) -> Self
// - amount(&self) -> f64
// - add(self, other: Self) -> Self
// - subtract(self, other: Self) -> Self

// Implement conversions:
// - Money<USD>::to_eur() -> Money<EUR>
// - Money<EUR>::to_usd() -> Money<USD>
// etc.

fn test_currency() {
    let usd = Money::<USD>::new(100.0);
    let eur = Money::<EUR>::new(85.0);
    
    // Should compile:
    let total_usd = usd.add(Money::<USD>::new(50.0));
    
    // Should NOT compile:
    // let mixed = usd.add(eur); // ERROR
    
    // Should compile after conversion:
    let eur_converted = usd.to_eur();
    let total_eur = eur.add(eur_converted);
}

Intermediate: File Permission System

Build a file permission system using phantom types.

// TODO: Implement a type-safe file permission system with:
// - Permission markers: ReadOnly, WriteOnly, ReadWrite
// - File<Permission> type
// - Only ReadWrite and ReadOnly can read
// - Only ReadWrite and WriteOnly can write
// - open_readonly, open_writeonly, open_readwrite methods
// - upgrade/downgrade permission methods
// - Ensure compile-time safety for all operations

// Starter code:
use std::marker::PhantomData;

struct ReadOnly;
struct WriteOnly;
struct ReadWrite;

struct File<Permission> {
    path: String,
    // Your code here
}

// Implement methods for specific permissions:
// impl File<ReadOnly> { fn read(&self) -> String }
// impl File<WriteOnly> { fn write(&mut self, data: &str) }
// impl File<ReadWrite> { fn read(&self) -> String, fn write(&mut self, data: &str) }

// Implement permission upgrades:
// impl File<ReadOnly> { fn upgrade_to_readwrite(self) -> File<ReadWrite> }

fn test_permissions() {
    let read_file = File::<ReadOnly>::open("data.txt");
    let data = read_file.read();
    // read_file.write("data"); // Should NOT compile
    
    let mut write_file = File::<WriteOnly>::open("output.txt");
    write_file.write("hello");
    // let data = write_file.read(); // Should NOT compile
    
    let mut rw_file = File::<ReadWrite>::open("both.txt");
    let data = rw_file.read();
    rw_file.write("new data");
}

Advanced: Type-Safe Database Query DSL

Create a type-level query builder that prevents invalid SQL at compile time.

// TODO: Implement a type-safe SQL query builder with:
// - Phantom types tracking query state (NoSelect, HasSelect, NoFrom, HasFrom, etc.)
// - Query<Select, From, Where, OrderBy> with phantom parameters
// - Methods that transition between states
// - Can only build() when query is complete (HasSelect + HasFrom)
// - Prevent calling select() twice, from() twice, etc.
// - Type-safe column references
// - Type-safe WHERE clause builder

// Starter code:
use std::marker::PhantomData;

// State markers
struct NoSelect;
struct HasSelect;
struct NoFrom;
struct HasFrom;
struct NoWhere;
struct HasWhere;

struct Query<Select, From, Where> {
    // Your code here
}

impl Query<NoSelect, NoFrom, NoWhere> {
    fn new() -> Self {
        // Initial query state
        todo!()
    }
}

// Implement state transitions:
// impl<F, W> Query<NoSelect, F, W> {
//     fn select(self, columns: &[&str]) -> Query<HasSelect, F, W>
// }

// impl<S, W> Query<S, NoFrom, W> {
//     fn from(self, table: &str) -> Query<S, HasFrom, W>
// }

// impl<S, F> Query<S, F, NoWhere> {
//     fn where_clause(self, condition: &str) -> Query<S, F, HasWhere>
// }

// Only complete queries can be built:
// impl Query<HasSelect, HasFrom, W> {
//     fn build(self) -> String
// }

fn test_query_builder() {
    // Should compile:
    let query = Query::new()
        .select(&["id", "name"])
        .from("users")
        .where_clause("age > 18")
        .build();
    
    // Should NOT compile (no select):
    // let bad = Query::new().from("users").build(); // ERROR
    
    // Should NOT compile (no from):
    // let bad = Query::new().select(&["id"]).build(); // ERROR
    
    // Should NOT compile (select twice):
    // let bad = Query::new().select(&["id"]).select(&["name"]); // ERROR
}

---

Real-World Usage in the Rust Ecosystem

1. std::marker::PhantomData

The standard library uses PhantomData extensively:

// Vec uses PhantomData for drop check
pub struct Vec<T> {
    buf: RawVec<T>,
    len: usize,
}

pub struct RawVec<T> {
    ptr: Unique<T>,
    cap: usize,
}

pub struct Unique<T: ?Sized> {
    pointer: *const T,
    _marker: PhantomData<T>, // Critical for soundness!
}

2. tokio - Async Runtime Lifetime Tracking

// tokio uses phantom lifetimes for safety
pub struct JoinHandle<T> {
    raw: RawTask,
    _p: PhantomData<fn() -> T>,
}

// Ensures proper lifetime relationships in async code
pub struct ReadHalf<'a> {
    inner: Arc<Inner>,
    _phantom: PhantomData<&'a mut Inner>,
}

3. diesel - Type-Safe SQL Queries

// diesel uses phantom types for query building
pub struct SelectStatement<
    From,
    Select = DefaultSelectClause,
    Distinct = NoDistinctClause,
    Where = NoWhereClause,
> {
    select: Select,
    from: From,
    distinct: Distinct,
    where_clause: Where,
    _marker: PhantomData<(From, Select, Distinct, Where)>,
}

// Type-safe query construction:
users::table
    .select(users::name)
    .filter(users::age.gt(18))
    // Compile error if you try invalid SQL!

4. serde - Deserialization with PhantomData

// serde uses PhantomData in deserializers
pub struct Deserializer<'de> {
    input: &'de str,
    _marker: PhantomData<&'de ()>,
}

// Zero-copy deserialization tracking
pub struct StrDeserializer<'a, E> {
    value: &'a str,
    marker: PhantomData<E>,
}

5. hyper - HTTP Protocol Versioning

// hyper tracks HTTP versions with phantom types
pub struct Request<T = Body> {
    head: RequestHead,
    body: T,
}

pub struct Response<T = Body> {
    head: ResponseHead,
    body: T,
}

// Version markers in type system
pub struct Http1;
pub struct Http2;

pub struct Connection<T, B> {
    protocol: Protocol,
    _marker: PhantomData<fn(T, B)>,
}

---

Further Reading

Official Documentation

Articles and Tutorials

Academic Papers

  • "Phantom Types and Subtyping" - Theoretical foundations
  • "Zero-Overhead Deterministic Exceptions" - Zero-cost abstractions
  • "Quantified Types in an Imperative Language" - Type-level programming

Related Patterns

  • Type-State Pattern - States as phantom types
  • Builder Pattern with Type-State - Compile-time validation
  • Newtype Pattern - Wrapping primitives with types
  • Session Types - Protocol enforcement with types

---

Summary

Phantom types are one of Rust's most powerful zero-cost abstraction techniques:

  • Type Safety: Prevent bugs at compile time through type system
  • Zero Cost: No runtime overhead - phantom types disappear completely
  • Compile-Time Guarantees: Make incorrect code impossible to write
  • PhantomData: Marker type for unused type parameters
  • Variance Control: Fine-tune subtyping relationships
  • Drop Check Safety: Ensure soundness in unsafe code

The key insight: Information in types costs nothing at runtime but provides immense safety benefits. Phantom types let you encode invariants that the compiler enforces but that leave zero trace in the final binary.

Use phantom types when you need type safety without runtime cost - units of measurement, type-safe IDs, validation states, protocol versioning, and ownership tracking. The pattern is especially powerful in systems programming where every byte and every cycle matters, but safety cannot be compromised.

Remember the NASA Mars Orbiter: a $327 million crash because of unit confusion. Phantom types make such bugs impossible - at zero runtime cost. That's the power of Rust's type system.

🎮 Try it Yourself

🎮

Phantom Types - Playground

Run this code in the official Rust Playground