Course contentsShow
← Rust
Lesson 723 of 1,9106. Generics and Static DispatchFree lesson

Generic State Machine Patterns

Use phantom types to encode state transitions at the type level to prevent invalid operations.

In this lesson, you will learn how to use PhantomData and generic type parameters to enforce valid state transitions at compile time, making illegal states impossible to represent.

The Problem: Logical State Errors

In many systems, objects have a lifecycle. For example, a Document might start as Draft and move to Published. If you use a simple enum to track this, you often have to check the state inside every method:

fn publish(&mut self) {
    if self.state == State::Published {
        panic!("Already published!"); // Runtime error
    }
    // ...
}

This is prone to human error. Wouldn't it be better if the compiler simply refused to compile code that tried to publish an already published document?

Encoding State in the Type

We can achieve this by making our struct generic over its state. We use "marker types"—empty structs that exist only to represent a state—and std::marker::PhantomData to tell the compiler that our struct "contains" that state, even though no data is actually stored in that type field.

use std::marker::PhantomData;

struct Draft;
struct Published;

struct Document<State> {
    content: String,
    _state: PhantomData<State>,
}

impl Document<Draft> {
    pub fn new(content: String) -> Self {
        Self { content, _state: PhantomData }
    }

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

Why This Works

Notice that the publish method consumes self (the Document<Draft>) and returns a Document<Published>. Because we haven't defined a publish method for Document<Published>, the compiler will throw an error if you try to call it twice.

By changing the type parameter from Draft to Published, we effectively destroy the old state and replace it with a new one. The logic is now baked into the type system, transforming potential runtime crashes into compile-time errors.

Key Takeaway: By using generic marker types and PhantomData, you can force the Rust compiler to verify that your state machine transitions follow your intended design rules before the program ever runs.