From 11481c74e4b66eeae4c1fdeb0862a3db8be0a16b Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sun, 3 Aug 2025 20:11:21 +0200 Subject: [PATCH] add first new draft for generic typestate --- .../typestate-pattern/typestate-advanced.md | 9 +- .../typestate-pattern/typestate-generics.md | 263 +++++++++++++++++- 2 files changed, 264 insertions(+), 8 deletions(-) diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md index f16521e4..0f95b5b5 100644 --- a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md @@ -76,10 +76,11 @@ impl SerializeList { + | serializer --> structure --> property --> list +-+ - | ^ | - V | | - +-----------+ - String + | | ^ | ^ + V | | | | + | +-----------+ | + String | | + +--------------------------+ ``` - From this diagram, we can observe: diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md index 1ad01edc..57af8c1d 100644 --- a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md @@ -1,13 +1,268 @@ ## Typestate Pattern with Generics -TODO +By combining typestate modeling with generics, we can express a wider range of +valid states and transitions without duplicating logic. This approach is +especially useful when the number of states grows or when multiple states share +behavior but differ in structure. -```rust,editable -// TODO +```rust +# use std::fmt::Write as _; +# +struct Serializer { + // [...] + # indent: usize, + # buffer: String, + # state: S, +} + +struct Root; +struct Struct(S); +struct List(S); +struct Property(S); + +impl Serializer { + fn new() -> Self { + // [...] + # Self { + # indent: 0, + # buffer: String::new(), + # state: Root, + # } + } + + fn serialize_struct(mut self, name: &str) -> Serializer> { + // [...] + # writeln!(self.buffer, "{name} {{").unwrap(); + # Serializer { + # indent: self.indent + 1, + # buffer: self.buffer, + # state: Struct(self.state), + # } + } + + fn finish(self) -> String { + // [...] + # self.buffer + } +} + +impl Serializer { + fn buffer_size(&self) -> usize { + // [...] + # self.buffer.len() + } +} + +impl Serializer> { + fn serialize_property(mut self, name: &str) -> Serializer>> { + // [...] + # write!(self.buffer, "{}{name}: ", " ".repeat(self.indent * 2)).unwrap(); + # Serializer { + # indent: self.indent, + # buffer: self.buffer, + # state: Property(self.state), + # } + } + + fn finish_struct(mut self) -> Serializer { + // [...] + # self.indent -= 1; + # writeln!(self.buffer, "{}}}", " ".repeat(self.indent * 2)).unwrap(); + # Serializer { + # indent: self.indent, + # buffer: self.buffer, + # state: self.state.0, + # } + } +} + +impl Serializer>> { + fn serialize_struct(mut self, name: &str) -> Serializer>> { + // [...] + # writeln!(self.buffer, "{name} {{").unwrap(); + # Serializer { + # indent: self.indent + 1, + # buffer: self.buffer, + # state: Struct(self.state.0), + # } + } + + fn serialize_list(mut self) -> Serializer>> { + // [...] + # writeln!(self.buffer, "[").unwrap(); + # Serializer { + # indent: self.indent + 1, + # buffer: self.buffer, + # state: List(self.state.0), + # } + } + + fn serialize_string(mut self, value: &str) -> Serializer> { + // [...] + # writeln!(self.buffer, "{value},").unwrap(); + # Serializer { + # indent: self.indent, + # buffer: self.buffer, + # state: self.state.0, + # } + } +} + +impl Serializer> { + fn serialize_struct(mut self, name: &str) -> Serializer>> { + // [...] + # writeln!(self.buffer, "{}{name} {{", " ".repeat(self.indent * 2)).unwrap(); + # Serializer { + # indent: self.indent + 1, + # buffer: self.buffer, + # state: Struct(self.state), + # } + } + + fn serialize_string(mut self, value: &str) -> Self { + // [...] + # writeln!(self.buffer, "{}{value},", " ".repeat(self.indent * 2)).unwrap(); + # self + } + + fn finish_list(mut self) -> Serializer { + // [...] + # self.indent -= 1; + # writeln!(self.buffer, "{}]", " ".repeat(self.indent * 2)).unwrap(); + # Serializer { + # indent: self.indent, + # buffer: self.buffer, + # state: self.state.0, + # } + } +} + +fn main() { + # #[rustfmt::skip] + let serializer = Serializer::new() + .serialize_struct("Foo") + .serialize_property("bar") + .serialize_struct("Bar") + .serialize_property("baz") + .serialize_list() + .serialize_string("abc") + .serialize_struct("Baz") + .serialize_property("partial") + .serialize_string("def") + .serialize_property("empty") + .serialize_struct("Empty") + .finish_struct() + .finish_struct() + .finish_list() + .finish_struct() + .finish_struct(); + + # let buffer_size = serializer.buffer_size(); + let output = serializer.finish(); + + # println!("buffer size = {buffer_size}\n---"); + println!("{output}"); + + // These will all fail at compile time: + + // Serializer::new().serialize_list(); + // Serializer::new().serialize_string("foo"); + // Serializer::new().serialize_struct("Foo").serialize_string("bar"); + // Serializer::new().serialize_struct("Foo").serialize_list(); + // Serializer::new().serialize_property("foo"); +} ```
-- TODO +- The full code for this example is available + [in the playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=48b106089ca600453f3ed00a0a31af26) + +- By using generics to track the parent context, we can construct arbitrarily + nested serializers that enforce valid transitions between struct, list, and + property states. + +- This lets us build a recursive structure while preserving control over what + methods are accessible in each state. + +- Here's how the flow maps to a state machine: + +```bob + +-----------+ +---------+------------+-----+ + | | | | | | + V | V | V | + + | +serializer --> structure --> property --> list +-+ + + | | ^ | ^ + V | | | | + | +-----------+ | + String | | + +--------------------------+ +``` + +- And this is reflected directly in the types of our serializer: + +```bob + +------+ + finish | | + serialize struct V | + struct ++---------------------+ --------------> +-----------------------------+ <---------------+ +| Serializer [ Root ] | | Serializer [ Struct [ S ] ] | | ++---------------------+ <-------------- +-----------------------------+ <-----------+ | + finish struct | | + | | serialize | | | + | +----------+ property V serialize | | + | | string or | | +finish | | +-------------------------------+ struct | | + V | | Serializer [ Property [ S ] ] | ------------+ | + finish | +-------------------------------+ | + +--------+ struct | | + | String | | serialize | | + +--------+ | list V | + | finish | + | +---------------------------+ list | + +------> | Serializer [ List [ S ] ] | ----------------+ + +---------------------------+ + serialize + list or string ^ + | or finish list | + +-------------------+ +``` + +- Of course, this pattern isn't a silver bullet. It still allows issues like: + - Empty or invalid property names (which can be fixed using + [the newtype pattern](../newtype-pattern.md)) + - Duplicate property names (which could be tracked in `Struct` or handled + via `Result`) + +- If validation failures occur, we can also change method signatures to return a + `Result`, allowing recovery: + + ```rust,compile_fail + struct PropertySerializeError { + kind: PropertyError, + serializer: Serializer>, + } + + impl Serializer> { + fn serialize_property( + self, + name: &str, + ) -> Result>>, PropertySerializeError> { + /* ... */ + } + } + ``` + +- While this API is powerful, it’s not always ergonomic. Production serializers + typically favor simpler APIs and reserve the typestate pattern for enforcing + critical invariants. + +- One excellent real-world example is + [`rustls::ClientConfig`](https://docs.rs/rustls/latest/rustls/client/struct.ClientConfig.html#method.builder), + which uses typestate with generics to guide the user through safe and correct + configuration steps.