3 min read
Typestate in Rust

Rust’s type-system allows for expressive relationships between types and restrictions in usage. With programming, I often think of my job as taking a primitive type, like u64 or Vec<Foo>, and sculpting an API that enforces semantic meaning around the type. The typestate pattern is an incredibly elegant way to do so.

The Goal

Sometimes we would like to restrict what methods a user is allowed to call based on internal state within a structure. For instance, we may want to restrict the methods a user can call if the client is not yet initialized. The typestate pattern meshes great with this design.

Example

Suppose we are designing a client that must be started before usage. We can define two states for the client, Idle and Active, and impose that the user must call a start method before using the other client methods.

First, we define a trait to represent the state of the client, and implement the trait for our two states. Finally, we want to restrict the user by not allowing them to implement the trait. To accomplish this we use a “sealed” trait that cannot be implemented outside our crate.

/// Client state when idle.
pub struct Idle;
/// Client state when active.
pub struct Active;

mod sealed {
    pub trait Sealed {}
}

impl sealed::Sealed for Idle {}
impl sealed::Sealed for Active {}

/// State of the client.
pub trait State: sealed::Sealed {}

impl State for Idle {}
impl State for Active {}

Now, we’ll have the client hold a state S.

pub struct Client<S: State> {
    _marker: core::marker::PhantomData<S>,
}

Finally, we can restrict the calls on Client based on S

impl Client<Idle> {
	pub fn start(mut self) -> Client<Active> {
		Client { _marker: core::marker::PhantomData }
	}
}

impl Client<Active> {
	pub fn foo(&self) -> String { .. }	
	pub fn bar(&self) -> u64 { .. }
}

When a user attempts to use a Client, if it has not been started, then the user cannot misuse the foo or bar method calls. Using the type system, we’ve restricted invalid states.

Drawbacks

The only drawback I see with this method is the new client state must be returned when transitioning states. This leaves the user with expressions like let client = client.start(). While this is clunkier compared to typical uses of &mut self, the tradeoff is worth the inconvenience when its critical to restrict client calls depending on states.

For a real-world example of where this is used, check out this PR or the Block type in rust-bitcoin.