"Easy to Use Correctly and Hard to Use Incorrectly" - The Typestate Pattern
How do you model a program so that it reflects real-world state, and that undesired or impossible states are unreachable?
Especially in safety-focused systems programming related to communication, interfaces, or I/O, the typestate pattern helps to ensure correctness and safety. Encoding state in type signatures exposes only valid transitions and operations. This reduces the need of runtime errors and even checks and leads to very pleasant APIs.
The Typestate Pattern in Rust
The Rust language has some unique features which enable downright beautiful typestate patterns. One idiomatic way to implement the state machine pattern is a struct with a generic parameter for each state. Such a
StateMachine<StateN> implements the operations of
<StateN>, and the initial state has a '
new' implementation. Transitions are realized by implementing
std::convert::From (automagically implementing
std::convert::Into). Due to the type signature of '
from', the previous state is 'moved' to the new state in a transition, becoming inaccessible. This, combined with type inference, is quite ergonomic and safe -- a transition can only happen when the owner has exclusive control. If the programmer requests an invalid state transition or operation,
rustc (the Rust Complainer) hints that a trait bound is not satisfied or a method is not implemented.
Best of all, those benefits are completely free in terms of runtime and memory! All typestate information is only used during compilation. The size of a marker struct, given by
std::mem::size_of::<StateN>(), is 0. However, in this case, there is a slight cost of language complexity and compiler cleverness.
Of course, a similar implementation is possible in
C++. But the lack of 'real' move semantics,
Into, type inference, good error messages, concise syntax, and ZST, lead to a challenging implementation of the pattern itself.
Every Rust programmer is familiar with this first example: ownership, mutable reference, and shared reference are the types of basic references in safe Rust. Each type defines their operations and transitions: Ownership allows dropping, and &mut implements the traits
BorrowMut, which shared references do not implement. Here, move semantics ensure that dropped or dangling references cannot exist.
Another example is the management of General-Purpose Input/Output Pins. Reflecting/influencing real-world state, they must be handled carefully. Each configuration (Input, Output, Push-Pull, Open-Drain, ...) has defined operations and transitions.
So, the typestate pattern is at the core of Rust, leads to good API design, and helps to model the real world.