diff --git a/src/SUMMARY.md b/src/SUMMARY.md index e09226bd..fc0d724d 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -441,6 +441,10 @@ - [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md) - [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md) - [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md) + - [Serializer: implement Root](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md) + - [Serializer: implement Struct](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md) + - [Serializer: implement Property](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md) + - [Serializer: Complete implementation](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md) --- 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 fd10ef5e..805d22a5 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 @@ -53,18 +53,7 @@ impl SerializeList { } ``` -
- -- Building on our previous serializer, we now want to support **nested - structures** and **lists**. - -- However, this introduces both **duplication** and **structural complexity**. - -- Even more critically, we now hit a **type system limitation**: we cannot - cleanly express what `finish()` should return without duplicating variants for - every nesting context (e.g. root, struct, list). - -- To better understand this limitation, let’s map the valid transitions: +Diagram of valid transitions: ```bob +-----------+ +---------+------------+-----+ @@ -80,7 +69,18 @@ serializer --> structure --> property --> list +-+ +--------------------------+ ``` -- From this diagram, we can observe: +
+ +- Building on our previous serializer, we now want to support **nested + structures** and **lists**. + +- However, this introduces both **duplication** and **structural complexity**. + +- Even more critically, we now hit a **type system limitation**: we cannot + cleanly express what `finish()` should return without duplicating variants for + every nesting context (e.g. root, struct, list). + +- From the diagram of valid transitions, we can observe: - The transitions are recursive - The return types depend on _where_ a substructure or list appears - Each context requires a return path to its parent 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 d27cdb43..401550b5 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 @@ -6,184 +6,19 @@ especially useful when the number of states grows or when multiple states share behavior but differ in structure. ```rust -# use std::fmt::Write as _; -# -struct Serializer { - // [...] - # indent: usize, - # buffer: String, - # state: S, -} +{{#include typestate-generics.rs:Serializer-def}} -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 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 output = serializer.finish(); - - 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"); -} +{{#include typestate-generics.rs:Root-def}} +{{#include typestate-generics.rs:Struct-def}} +{{#include typestate-generics.rs:Property-def}} +{{#include typestate-generics.rs:List-def}} ``` -
+We now have all the tools needed to implement the methods for the `Serializer` +and its state type definitions. This ensures that our API only permits valid +transitions, as illustrated in the following diagram: -- 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. - -- Methods common to all states can be implemented for any `S` in - `Serializer`. - -- These marker types (e.g., `List`) incur no memory or runtime overhead, as - they hold no data other than a possible Zero-Sized Type. Their sole purpose is - to enforce correct API usage by leveraging the type system. - -- Here's how the flow maps to a state machine: +Diagram of valid transitions: ```bob +-----------+ +---------+------------+-----+ @@ -199,68 +34,19 @@ serializer --> structure --> property --> list +-+ +--------------------------+ ``` -- 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 | - +-------------------+ -``` +- By leveraging generics to track the parent context, we can construct + arbitrarily nested serializers that enforce valid transitions between struct, + list, and property states. -- 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` and handled - via `Result`) +- This enables us to build a recursive structure while maintaining strict + control over which methods are accessible in each state. -- If validation failures occur, we can also change method signatures to return a - `Result`, allowing recovery: +- Methods common to all states can be defined for any `S` in `Serializer`. - ```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. +- Marker types (e.g., `List`) introduce no memory or runtime overhead, as + they contain no data other than a possible Zero-Sized Type. Their only role is + to enforce correct API usage through the type system.
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.rs b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.rs new file mode 100644 index 00000000..25587c35 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.rs @@ -0,0 +1,164 @@ +// ANCHOR: Complete +use std::fmt::Write as _; + +// ANCHOR: Serializer-def +struct Serializer { + // [...] + indent: usize, + buffer: String, + state: S, +} +// ANCHOR_END: Serializer-def + +// ANCHOR: Root-def +struct Root; +// ANCHOR_END: Root-def + +// ANCHOR: Struct-def +struct Struct(S); +// ANCHOR_END: Struct-def + +// ANCHOR: List-def +struct List(S); +// ANCHOR_END: List-def + +// ANCHOR: Property-def +struct Property(S); +// ANCHOR_END: Property-def + +// ANCHOR: Root-impl +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 + } +} +// ANCHOR_END: Root-impl + +// ANCHOR: Struct-impl +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 } + } +} +// ANCHOR_END: Struct-impl + +// ANCHOR: Property-impl +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 } + } +} +// ANCHOR_END: Property-impl + +// ANCHOR: List-impl +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 } + } +} +// ANCHOR_END: List-impl + +// ANCHOR: main +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 output = serializer.finish(); + + 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"); +} +// ANCHOR_END: main diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md new file mode 100644 index 00000000..066a7cc1 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md @@ -0,0 +1,89 @@ +## Serializer: complete implementation + +Looking back at our original desired flow: + +```bob + +-----------+ +---------+------------+-----+ + | | | | | | + V | V | V | + + | +serializer --> structure --> property --> list +-+ + + | | ^ | ^ + V | | | | + | +-----------+ | + String | | + +--------------------------+ +``` + +We can now see this 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 | + +-------------------+ +``` + +The code for the full implementation of the `Serializer` and all its states can +be found in +[this Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=c9cbb831cd05fe9db4ce42713c83ca16). + +
+ +- 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` and 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. + +
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md new file mode 100644 index 00000000..31da577a --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md @@ -0,0 +1,49 @@ +## Serializer: implement Property + +```rust +# use std::fmt::Write as _; +{{#include ../typestate-generics.rs:Serializer-def}} + +{{#include ../typestate-generics.rs:Struct-def}} +{{#include ../typestate-generics.rs:Property-def}} +{{#include ../typestate-generics.rs:List-def}} + +{{#include ../typestate-generics.rs:Property-impl}} +``` + +With the addition of the Property state methods, our diagram is now nearly +complete: + +```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 ] ] | ------------+ + +-------------------------------+ + +--------+ + | String | serialize | + +--------+ list V + + +---------------------------+ + | Serializer [ List [ S ] ] | + +---------------------------+ +``` + +
+ +- A property can be defined as a `String`, `Struct`, or `List`, enabling + the representation of nested structures. + +- This concludes the step-by-step implementation. The full implementation, + including support for `List`, is shown in the next slide. + +
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md new file mode 100644 index 00000000..de1b9087 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md @@ -0,0 +1,40 @@ +## Serializer: implement Root + +```rust +# use std::fmt::Write as _; +{{#include ../typestate-generics.rs:Serializer-def}} + +{{#include ../typestate-generics.rs:Root-def}} +{{#include ../typestate-generics.rs:Struct-def}} + +{{#include ../typestate-generics.rs:Root-impl}} +``` + +Referring back to our original diagram of valid transitions, we can visualize +the beginning of our implementation as follows: + +```bob + serialize + struct ++---------------------+ --------------> +--------------------------------+ +| Serializer [ Root ] | | Serializer [ Struct [ Root ] ] | ++---------------------+ <-------------- +--------------------------------+ + finish struct + | + | + | +finish | + V + + +--------+ + | String | + +--------+ +``` + +
+ +- At the "root" of our `Serializer`, the only construct allowed is a `Struct`. + +- The `Serializer` can only be finalized into a `String` from this root level. + +
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md new file mode 100644 index 00000000..7931c50f --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md @@ -0,0 +1,43 @@ +## Serializer: implement Struct + +```rust +# use std::fmt::Write as _; +{{#include ../typestate-generics.rs:Serializer-def}} + +{{#include ../typestate-generics.rs:Struct-def}} +{{#include ../typestate-generics.rs:Property-def}} + +{{#include ../typestate-generics.rs:Struct-impl}} +``` + +The diagram can now be expanded as follows: + +```bob + +------+ + finish | | + serialize struct V | + struct ++---------------------+ --------------> +-----------------------------+ +| Serializer [ Root ] | | Serializer [ Struct [ S ] ] | ++---------------------+ <-------------- +-----------------------------+ + finish struct + | serialize | + | property V + | +finish | +------------------------------------------+ + V | Serializer [ Property [ Struct [ S ] ] ] | + +------------------------------------------+ + +--------+ + | String | + +--------+ +``` + +
+ +- A `Struct` can only contain a `Property`; + +- Finishing a `Struct` returns control back to its parent, which in our previous + slide was assumed the `Root`, but in reality however it can be also something + else such as `Struct` in case of nested "structs". + +