Course contentsShow
← Rust
Lesson 399 of 1,9104. Lifetimes and Advanced OwnershipFree lesson

Type-Level Programming for Lifetimes

Use phantom types to enforce state machine correctness at compile time.

Type-Level Programming for Lifetimes

In this lesson, you will learn how to use PhantomData and type markers to turn your state transitions into compile-time guarantees, ensuring that specific objects can only exist during valid parts of a lifetime.

The Problem: Illegal Transitions

Imagine a Document state machine where a document must be Draft before it can be Published. Normally, you might track this with an enum and an if check at runtime. But if your Document holds a reference to a parent Library (with a lifetime 'a), we can enforce that the document is only "active" while the library exists, and only "publishable" if it has reached a specific state.

The Solution: Phantom Types

By tagging our struct with a type parameter, we can define transitions that the compiler checks for us. The PhantomData marker allows us to "pretend" our struct holds a specific type without actually storing it.

use std::marker::PhantomData;

struct Draft;
struct Published;

struct Document<'a, State> {
    content: &'a str,
    _state: PhantomData<State>,
}

impl<'a> Document<'a, Draft> {
    fn new(content: &'a str) -> Self {
        Self { content, _state: PhantomData }
    }

    fn publish(self) -> Document<'a, Published> {
        Document { content: self.content, _state: PhantomData }
    }
}

// This function only accepts Published documents!
fn send_to_printer(doc: Document<'_, Published>) {
    println!("Printing: {}", doc.content);
}

How it Works

  1. Zero-Cost Abstraction: The PhantomData<State> takes up zero bytes at runtime. It exists purely for the compiler’s type checker.
  2. Lifetime Binding: Because Document is tied to 'a, the compiler knows that any Document (whether Draft or Published) cannot outlive the string it references.
  3. State Locking: By defining publish to consume self and return a Document<Published>, we make it impossible to call send_to_printer with a Draft. The type system prevents the "illegal transition" before the code ever runs.

By embedding these markers into our types, we treat the state machine as a set of logical proofs. If the code compiles, the state transition is valid, and the reference is guaranteed to be safe for the entire duration of the document's life.

Key Takeaway: Use PhantomData to encode state transitions as types, allowing the compiler to verify that your resources are used only in valid states throughout their lifetime.