Skip to content

Instantly share code, notes, and snippets.

@barafael
Created July 26, 2020 21:23
Show Gist options
  • Save barafael/0cbb0d89e7a9c0ada5744848cb3aa2fe to your computer and use it in GitHub Desktop.
Save barafael/0cbb0d89e7a9c0ada5744848cb3aa2fe to your computer and use it in GitHub Desktop.
Short piece about the typestate pattern in Rust

"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, From/Into, type inference, good error messages, concise syntax, and ZST, lead to a challenging implementation of the pattern itself.

Examples

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 DerefMut and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment