Type State Pattern

Safe API

Imagine we are writing a library for a game.

For this library, we will expose a player object. The player object will have the following methods:

  • new() -> to create new Player object
  • buy_item()
  • sell_item()
  • list_items()
  • die()
  • get_resurrected()


pretty usual stuff so far. Where things get interesting is here: we want some methods to be callable on the dead player objects, some on the alive player objects, and some on the both.

Dead:

  • get_resurrected()


Alive:

  • die()
  • sell_item()
  • buy_item()
  • new() -> this may seem weird, but newly created player should be alive, hence, it belongs here


both:

  • list_items() -> we can list the items of the dead players, why not…

Designing the API

Say, our Player object is defined as such:

struct Player {
    username: String,
    health: u8,
    items: Vec<String>,
}

now, if we implement all the above methods for the Player struct, like this:

impl Player {
    fn die(&self) {
        todo!()
    }

    // and other methods below
}

we can call die() multiple times on the Player, which makes no sense.

I’ll use die() as an example throughout this article.

But the same principles apply to all other methods.

Going back to our API, it is definitely not safe! We should do better…


Solution candidate 1:

we can introduce an additional field to the struct alive:

struct Player {
    username: String,
    health: u8,
    items: Vec<String>,
    alive: bool // it is here in case you are blind
}


and we can do runtime checks when necessary:

impl Player {
    fn die(&self) {
        if self.alive {
            todo!()
        } else {
            panic!("🤷‍♀️ cannot die twice 🤷‍♀️")
        }
    }
}


this will work… But it’s disgusting. And surprisingly, not only because of the emoji:

  • runtime check? we can do compile-time check 🤓
  • we introduced another field to the struct and now it will occupy more memory (kind of tolerable)
  • we increased runtime complexity. I hate the fact that we will now do an extra check before each call to the methods of Player to be able to determine if we can call them in the first place… (intolerable)

Solution candidate 2:

One approach for achieving compile-time checks, might be having two different structs for alive and dead player, and have the necessary methods implemented for them respectively.

struct AlivePlayer {
    username: String,
    health: u8,
    items: Vec<String>,
}

struct DeadPlayer {
    username: String,
    health: u8,
    items: Vec<String>,
}

impl DeadPlayer {
    // no die method here
}

impl AlivePlayer {
    fn die(self) -> DeadPlayer  {
        todo!()
    }
}


better than the previous solution. But still bad

why it’s bad:

  • the API is not clean. We are storing the same fields in both Dead and Alive player, while they are both just Players.
  • the end-user has to know when to create an instance of Alive player and Dead player. It might be simple to guess in this example, but imagine much more complex/abstract types. If possible, our API should be responsible for when to use which type, not the end user.

Solution candidate 3:

This is called type state pattern, and I really like it.

We will introduce type as a generic parameter for our Player struct, and we will define methods accordingly.

struct Player<State> {
    username: String,
    health: u8,
    items: Vec<String>,
    state: PhantomData<State>, // i'll explain this, wait
}

struct Alive;
struct Dead;

impl Player<Alive> {
    pub fn die(self) -> Player<Dead> {
        Player {
            username: self.username,
            health: 0,
            items: self.items,
            state: PhantomData,
        }
    }

    // and other methods
}

impl Player<Dead> {
    // no `die` method here :)
}


What’s going on here:

  • by making our struct generic, we can have an alive and a dead version of it.
  • alive and dead are represented by unit structs, allowing us to use them as types.
  • die() method is only implemented for the Player<Alive>, and it is returning Player<Dead> after consuming the self. In other words, alive player is gone.
  • for the Player<Dead>, we don’t have the die() method, making it impossible to call die() on a dead player.
  • we achieve compile-time warranty of die() method being not callable on a dead player.

What the hell is state: PhantomData<State> in the Player struct?:

If I hadn’t put that, it wouldn’t compile.

Rust does not allow us to declare struct Player<State> and not use State in any of the fields.

But, we don’t want to use the State in the fields. We don’t want it to put extra memory on runtime.

Luckily, PhantomData is is a zero-sized-type, meaning that, it won’t be in the compiled code. No runtime effects will be present, totally zero cost! It just acts like it owns/uses State, so that compiler can shut up about:

In the end, we did introduce a new field to the Player struct, but only in compile-time. It does not exist in runtime.

Very cool, but not cool enough.

The small problem:

What happens if the user wanted to mess around and provided their own type to the Player struct:

struct Player<State> {
    username: String,
    health: u8,
    items: Vec<String>,
    state: PhantomData<State>,
}

fn main() {
    let player = Player::<u8> {
        username: "potato".to_string(),
        health: 100,
        items: vec!["frostmourne".to_string()],
        state: PhantomData,
    };
}



The Final Solution

By applying a trait bound to the generic, we can also solve the above problem:

struct Alive;
struct Dead;

trait PlayerState {}

impl PlayerState for Alive {}
impl PlayerState for Dead {}

struct Player<State>
where
    State: PlayerState,
{
    username: String,
    health: u8,
    items: Vec<String>,
    state: PhantomData<State>,
}


Now, the user won’t be able to do this:

let player = Player::<u8> {
        username: "potato".to_string(),
        health: 100,
        items: vec!["frostmourne".to_string()],
        state: PhantomData,
    };



Full Code

Here, have a big chunk of code:

#![allow(unused)]

use std::marker::PhantomData;

struct Alive;
struct Dead;

trait PlayerState {}

impl PlayerState for Alive {}
impl PlayerState for Dead {}

struct Player<State = Alive>
where
    State: PlayerState,
    // or `Player<State: PlayerState = Alive>`
{
    username: String,
    health: u8,
    items: Vec<String>,
    state: PhantomData<State>,
}

impl Player<Alive> {
    pub fn die(self) -> Player<Dead> {
        Player {
            username: self.username,
            health: 0,
            items: self.items,
            state: PhantomData,
        }
    }

    pub fn sell_item(&mut self, item_idx: usize) {
        &self.items.remove(item_idx);
    }

    pub fn buy_item(&mut self, item: String) {
        self.items.push(item);
    }
}

impl Player<Dead> {
    pub fn get_resurrected(self) -> Player<Alive> {
        Player {
            username: self.username,
            health: 100,
            items: self.items,
            state: PhantomData,
        }
    }
}

impl<State: PlayerState> Player<State> {
    pub fn list_items(&self) -> &[String] {
        self.items.as_slice()
    }
}

impl Player {
    pub fn new(username: String) -> Self {
        Player {
            username,
            health: 100,
            items: Default::default(),
            state: PhantomData,
        }
    }
}

fn main() {
    let mut player = Player::new("HiMom".to_owned()); // create an `Alive` player

    /* the below commented out code creates compile time error */
    /*****************/
    // player.get_resurrected(); // no method named `get_resurrected()` found for struct `Player` in the current scope

    let player = player.die(); // we can kill a an `Alive` player

    /*  CANNOT kill what's already dead */
    /*****************/
    // player.kill(); // no method named `kill` found for struct `Player<Dead>` in the current scope

    player.list_items(); // can list items for `Alive` or `Dead` player
    let mut player = player.get_resurrected(); // can resurrect a `Dead` player
    player.list_items(); // can list items for `Alive` or `Dead` player;

    /* we can ensure the TraitBound also in compile-time, the below code creates the error: */
    /*  the trait bound `u8: PlayerState` is not satisfied */
    /*****************/
    // let invalid_player = Player::<u8> {
    //     username: "potato".to_string(),
    //     health: 100,
    //     items: vec!["frostmourne".to_string()],
    //     state: PhantomData,
    // };
}



Some notes on the code

First note:

if this is new syntax to you: struct Player<State = Alive>, it means, if the generic type is not mentioned, assume the default value would be Alive.

So, this code:

impl Player {
    pub fn new(username: String) -> Self {
        Player {
            username,
            health: 100,
            items: Default::default(),
            state: PhantomData,
        }
    }
}


does not mention the generic, and thus, it is actually equivalent to:

impl Player<Alive> { // notice the `Alive` here
    pub fn new(username: String) -> Self {
        Player {
            username,
            health: 100,
            items: Default::default(),
            state: PhantomData,
        }
    }
}


Then, why did I not put new method in impl Player<Alive> block next to other alive methods? Because, I like constructors in a separate place, completely subjective reason. Do as you please.

Second note:

impl<State: PlayerState> Player<State> {
    pub fn list_items(&self) -> &[String] {
        self.items.as_slice()
    }
}


this means, as long as State implements PlayerState trait, list_items should be available. In other words, list_items is only available for Alive and Dead players.