Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Idiomatic Rust code guidance based on Apollo GraphQL's best practices handbook for ownership, errors, and performance.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/chapter_07.md
1# Chapter 7 - Type State Pattern23Models state at compile time, preventing bugs by making illegal states unrepresentable. It takes advantage of the Rust generics and type system to create sub-types that can only be reached if a certain condition is achieved, making some operations illegal at compile time.45> Recently it became the standard design pattern of Rust programming. However, it is not exclusive to Rust, as it is achievable and has inspired other languages to do the same [swift](https://swiftology.io/articles/typestate/) and [typescript](https://catchts.com/type-state).67## 7.1 What is Type State Pattern?89**Type State Pattern** is a design pattern where you encode different **states** of the system as **types**, not as runtime flags or enums. This allows the compiler to enforce state transitions and prevent illegal actions at compile time. It also improves the developer experience, as developers only have access to certain functions based on the state of the type.1011> Invalid states become compile errors instead of runtime bugs.1213## 7.2 Why use it?1415* Avoids runtime checks for state validity. If you reach certain states, you can make certain assumptions of the data you have.16* Models state transitions as type transitions. This is similar to a state machine, but in compile time.17* Prevents data misuse, e.g. using uninitialized objects.18* Improves API safety and correctness.19* The phantom data field is removed after compilation so no extra memory is allocated.2021## 7.3 Simple Example: File State2223[Github Example](https://github.com/apollographql/rust-best-practices/tree/main/examples/simple-type-state)24```rust25use std::{io, path::{Path, PathBuf}};2627struct FileNotOpened;28struct FileOpened;2930#[derive(Debug)]31struct File<State> {32/// Path to the opened file33path: PathBuf,34/// Open `File` handler35handle: Option<std::fs::File>,36/// Type state manager37_state: std::marker::PhantomData<State>38}3940impl File<FileNotOpened> {41/// `open` is the only entry point for this struct.42/// * When called with a valid path, it will return a `File<FileOpened>` with a valid `handler` and `path`43/// * `open` serves as an alternative to `new` and `defaults` methods (usable when your struct needs valid data to exist).44fn open(path: &Path) -> io::Result<File<FileOpened>> {45// If file is invalid, it will return `std::io::Error`46let file = std::fs::File::open(path)?;47Ok(48File {49path: path.to_path_buf(),50// Always valid51handle: Some(file),52_state: std::marker::PhantomData::<FileOpened>53}54)55}56}5758impl File<FileOpened> {59/// Reads the content of the `File` as a `String`.60/// `read` can only be called by state `File<FileOpened>`61fn read(&mut self) -> io::Result<String> {62use io::Read;6364let mut content = String::new();65let Some(handle)= self.handle.as_mut() else {66unreachable!("Safe to unwrap as state can only be reached when file is open");67};68handle.read_to_string(&mut content)?;69Ok(content)70}7172/// Returns the valid path buffer.73fn path(&self) -> &PathBuf {74&self.path75}76}77```7879## 7.4 Real-World Examples8081### Builder Pattern with Compile-Time Guarantees8283> Forces the user to **set required fields** before calling `.build()`.8485[Github Example](https://github.com/apollographql/rust-best-practices/tree/main/examples/type-state-builder)8687A type-state pattern can have more than one associated states:8889```rust90use std::marker::PhantomData;9192struct MissingName;93struct NameSet;94struct MissingAge;95struct AgeSet;9697#[derive(Debug)]98struct Person {99name: String,100age: u8,101email: Option<String>,102}103104struct Builder<NameState, AgeState> {105name: Option<String>,106age: u8,107email: Option<String>,108_name_marker: PhantomData<NameState>,109_age_marker: PhantomData<AgeState>,110}111112impl Builder<MissingName, MissingAge> {113fn new() -> Self {114Builder { name: None, age: 0, _name_marker: PhantomData, _age_marker: PhantomData, email: None }115}116117fn name(self, name: String) -> Builder<NameSet, MissingAge> {118Builder { name: Some(name), _name_marker: PhantomData::<NameSet>, age: self.age, _age_marker: PhantomData, email: None }119}120121fn age(self, age: u8) -> Builder<MissingName, AgeSet> {122Builder { age, _age_marker: PhantomData::<AgeSet>, name: None, _name_marker: PhantomData, email: None }123}124}125126impl Builder<NameSet, MissingAge> {127fn age(self, age: u8) -> Builder<NameSet, AgeSet> {128Builder { age, _age_marker: PhantomData::<AgeSet>, name: self.name, _name_marker: PhantomData::<NameSet>, email: None }129}130}131132impl Builder<MissingName, AgeSet> {133fn email(self, email: String) -> Self {134Self { name: self.name , age: self.age , email: Some(email) , _name_marker: self._name_marker , _age_marker: self._age_marker }135}136137fn name(self, name: String) -> Builder<NameSet, AgeSet> {138Builder { name: Some(name), _name_marker: PhantomData::<NameSet>, age: self.age, _age_marker: PhantomData::<AgeSet>, email: self.email }139}140}141142impl Builder<NameSet, AgeSet> {143fn build(self) -> Person {144Person {145name: self.name.unwrap_or_else(|| unreachable!("Name is guarantee to be set")),146age: self.age,147email: self.email,148}149}150}151```152153Although a bit more verbose than a usual builder, this guarantees that all necessary fields are present (note that e-mail is optional field only present in the final builder).154155#### Usage:156```rust157// โ Valid cases158let person: Person = Builder::new().name("name".to_string()).age(30).build();159let person: Person = Builder::new().age(30).name("name".to_string()).build();160let person: Person = Builder::new().age(30).name("name".to_string()).email("[email protected]".to_string()).build();161162// โ Invalid cases163let person: Person = Builder::new().name("name".to_string()).build(); // โ Compile error: Age required to `build`164let person: Person = Builder::new().age(30).build(); // โ Compile error: Name required to `build`165let person: Person = Builder::new().age(30).email("[email protected]".to_string()).build(); // โ Compile error: Name required to `build`166let person: Person = Builder::new().build();// โ Compile error: Name and Age required to `build`167```168169### Network Protocol State Machine170171Illegal transitions like sending a message before connecting **simply don't compile**:172173```rust174// Mock example175struct Disconnected;176struct Connected;177178struct Client<State> {179stream: Option<std::net::TcpStream>,180_state: std::marker::PhantomData<State>181}182183impl Client<Disconnected> {184fn connect(addr: &str) -> std::io::Result<Client<Connected>> {185let stream = std::net::TcpStream::connect(addr)?;186Ok(Client {187stream: Some(stream),188_state: std::marker::PhantomData::<Connected>189})190}191}192193impl Client<Connected> {194fn send(&mut self, msg: &str) {195use std::io::Write;196let Some(stream) = self.stream.as_mut() else {197unreachable!("Stream is guarantee to be set");198};199stream.write_all(msg.as_bytes())200}201}202```203204## 7.5 Pros and Cons205206### โ Use Type-State Pattern When:207* Your want **compile-time state safety**.208* You need to enforce **API constraints**.209* You are writing a library/crate that is heavy dependent on variants.210* Your want to replace runtime booleans or enums with **type-safe code paths**.211* You need compile time correctness.212213### โ Avoid it when:214* Writing trivial states like enums.215* Don't need type-safety.216* When it leads to overcomplicated generics.217* When runtime flexibility is required.218219### ๐จ Downsides and Cautions220* Can lead to more **verbose solutions**.221* Can lead to **complex type signatures**.222* May require **unsafe** to return **variant outputs** based on different states.223* May required a bunch of duplication (e.g. same struct field reused).224* PhantomData is not intuitive for beginners and can feel a bit hacky.225226> Use this pattern when it **saves bugs, increases safety or simplifies logic**, not just for cleverness.227