RustArena

Step 6 of 7

PhantomData and typestate

Type-level programming means letting types carry information the compiler checks, so whole classes of bugs become compile errors. The most useful form at work is typestate: the state of an object is part of its type, and each state has only the methods that make sense in it.

use std::marker::PhantomData;

struct Locked;
struct Closed;
struct Open;

struct Door<State> {
    name: String,
    _state: PhantomData<State>,
}

impl Door<Locked> {
    fn new(name: &str) -> Door<Locked> {
        Door { name: name.to_string(), _state: PhantomData }
    }
    fn unlock(self) -> Door<Closed> {
        println!("{}: unlocked", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

impl Door<Closed> {
    fn open(self) -> Door<Open> {
        println!("{}: opened", self.name);
        Door { name: self.name, _state: PhantomData }
    }
    fn lock(self) -> Door<Locked> {
        println!("{}: locked", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

impl Door<Open> {
    fn close(self) -> Door<Closed> {
        println!("{}: closed", self.name);
        Door { name: self.name, _state: PhantomData }
    }
}

fn main() {
    let door = Door::new("vault");
    let door = door.unlock().open();
    let door = door.close();
    let _door = door.lock();
    println!("{}", std::mem::size_of::<Door<Open>>() == std::mem::size_of::<String>());
}
vault: unlocked
vault: opened
vault: closed
vault: locked
true

How it works:

Trying to open a locked door isn't a runtime error; it doesn't compile:

use std::marker::PhantomData;

struct Locked;
struct Closed;

struct Door<S> {
    name: String,
    _state: PhantomData<S>,
}

impl Door<Locked> {
    fn new(name: &str) -> Self {
        Door { name: name.to_string(), _state: PhantomData }
    }
}

impl Door<Closed> {
    fn open(self) -> String {
        self.name
    }
}

fn main() {
    let door = Door::new("vault");
    println!("{}", door.open());
}
error[E0599]: no method named `open` found for struct `Door<Locked>` in the current scope

Why PhantomData?

Rust rejects a type parameter that no field uses: struct Door<State> { name: String } fails with error[E0392]: type parameter `State` is never used. PhantomData<State> (from std::marker) is a field of size zero that says "act as if this struct holds a State". It costs nothing at runtime, as the size_of line shows: the door is exactly as big as its String. You create it with the value PhantomData.

Where you'll meet typestate

Builders that can't be finished before the required fields are set, connections that can't send before they're authenticated, and hardware drivers (embedded Rust uses it so a pin configured as an input has no set_high method). It's worth it when misuse is costly; for simple cases an enum with a runtime check is easier to read. The patterns module comes back to this.

Your task

Finish a typestate HTTP request builder that can't be sent until a URL is set. RequestBuilder::new() starts as RequestBuilder<NoUrl> with method GET. Add:

Challenge 2

A blog post goes Draft, then Review, then Published. Finish the typestate Post:

Challenge 3

Write a database connection with three states from scratch. Use marker structs Disconnected, Connected and Authenticated and a Connection<S> holding a host: String and a PhantomData<S>.

Previous: Newtypes, the orphan rule and type aliases Next: The never type and Sized