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

Encoding Logic Constraints in Types

Prevent invalid program states at the compiler level using phantom data and generic bounds.

In this lesson, you will learn how to shift logic errors from runtime crashes to compile-time failures by using type-level markers.

The Problem with Runtime Flags

Imagine you are building a system to manage a rocket launch. You might have a Rocket struct with a launched boolean field. To prevent calling "launch" twice, you’d check if self.launched { panic!() }. But what if you forget that check? The error only appears when the program runs.

The State-Machine Approach

Instead of runtime flags, we can use empty types (often called "marker types") and PhantomData to encode the state directly into the type signature. The compiler then refuses to build the code if you perform an invalid transition.

use std::marker::PhantomData;

// Marker types
struct Ready;
struct Launched;

// A generic struct that tracks its own state
struct Rocket<State> {
    _state: PhantomData<State>,
}

impl Rocket<Ready> {
    fn new() -> Self {
        Rocket { _state: PhantomData }
    }

    fn launch(self) -> Rocket<Launched> {
        println!("Ignition!");
        Rocket { _state: PhantomData }
    }
}

impl Rocket<Launched> {
    fn abort(self) -> Rocket<Ready> {
        println!("Aborting...");
        Rocket { _state: PhantomData }
    }
}

Why This Works

Because launch consumes the Rocket<Ready> and returns a Rocket<Launched>, you literally cannot call launch twice on the same object—it no longer exists after the first call. If you try to call abort on a Rocket<Ready>, the Rust compiler will throw an error because the method is not defined for that specific generic type.

We use PhantomData<State> because the struct doesn't actually store a value of type State; it just needs to "know" which state it is in to satisfy the compiler's type checking. This effectively turns your business logic into a type-checking exercise that the compiler solves for you. By encoding these constraints, you eliminate the possibility of invalid state transitions entirely.

Key Takeaway: By using generic marker types, you can force the compiler to prove that your program logic is sound, turning potential runtime bugs into impossible code.