In this lesson, you will learn how to use Rust’s type system to enforce strict protocol sequences, ensuring your program can never reach an illegal state.
The Problem of Invalid Transitions
In network protocols or hardware drivers, operations often follow a rigid sequence. For example, a socket might need to be Initialized before it can be Connected, and Connected before it can Transmit. In many languages, you track this with a state variable and if checks at runtime. In Rust, we can move these checks to compile time, making illegal transitions literally impossible to represent.
Modeling States as Types
Instead of a single struct with a runtime state field, we define each state as its own empty "marker" struct. We then use methods on these structs that consume self and return the next state in the sequence.
struct Initialized;
struct Connected { address: String }
struct Socket<State> {
state: State,
}
impl Socket<Initialized> {
fn connect(self, addr: String) -> Socket<Connected> {
Socket { state: Connected { address: addr } }
}
}
impl Socket<Connected> {
fn send(self, data: &str) {
println!("Sending to {}: {}", self.state.address, data);
}
}
Why This Works
Notice that the send method is only defined for Socket<Connected>. If you try to call socket.send() on an Initialized socket, the compiler will look for a send method on Socket<Initialized>, fail to find one, and stop your build.
By consuming self, we ensure that the old state is destroyed when the new state is created. The transition is "atomic" at the type level: you cannot have a Connected state without having successfully performed the connect operation. This turns your protocol requirements into a series of type-level constraints that the compiler verifies for you, eliminating the need for if state == State::Connected error-handling boilerplate.
Key Takeaway: By encoding protocols into type-state sequences, we shift the burden of verifying state validity from runtime checks to the compiler, making invalid protocol transitions impossible to compile.