1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-08-08 08:22:52 +02:00

add first new draft for generic typestate

This commit is contained in:
Glen De Cauwsemaecker
2025-08-03 20:11:21 +02:00
parent b61c3378c5
commit 11481c74e4
2 changed files with 264 additions and 8 deletions

View File

@ -76,10 +76,11 @@ impl SerializeList {
+ |
serializer --> structure --> property --> list +-+
| ^ |
V | |
+-----------+
String
| | ^ | ^
V | | | |
| +-----------+ |
String | |
+--------------------------+
```
- From this diagram, we can observe:

View File

@ -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<S> {
// [...]
# indent: usize,
# buffer: String,
# state: S,
}
struct Root;
struct Struct<S>(S);
struct List<S>(S);
struct Property<S>(S);
impl Serializer<Root> {
fn new() -> Self {
// [...]
# Self {
# indent: 0,
# buffer: String::new(),
# state: Root,
# }
}
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<Root>> {
// [...]
# writeln!(self.buffer, "{name} {{").unwrap();
# Serializer {
# indent: self.indent + 1,
# buffer: self.buffer,
# state: Struct(self.state),
# }
}
fn finish(self) -> String {
// [...]
# self.buffer
}
}
impl<S> Serializer<S> {
fn buffer_size(&self) -> usize {
// [...]
# self.buffer.len()
}
}
impl<S> Serializer<Struct<S>> {
fn serialize_property(mut self, name: &str) -> Serializer<Property<Struct<S>>> {
// [...]
# 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<S> {
// [...]
# self.indent -= 1;
# writeln!(self.buffer, "{}}}", " ".repeat(self.indent * 2)).unwrap();
# Serializer {
# indent: self.indent,
# buffer: self.buffer,
# state: self.state.0,
# }
}
}
impl<S> Serializer<Property<Struct<S>>> {
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<Struct<S>>> {
// [...]
# writeln!(self.buffer, "{name} {{").unwrap();
# Serializer {
# indent: self.indent + 1,
# buffer: self.buffer,
# state: Struct(self.state.0),
# }
}
fn serialize_list(mut self) -> Serializer<List<Struct<S>>> {
// [...]
# 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<Struct<S>> {
// [...]
# writeln!(self.buffer, "{value},").unwrap();
# Serializer {
# indent: self.indent,
# buffer: self.buffer,
# state: self.state.0,
# }
}
}
impl<S> Serializer<List<S>> {
fn serialize_struct(mut self, name: &str) -> Serializer<Struct<List<S>>> {
// [...]
# 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<S> {
// [...]
# 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");
}
```
<details>
- 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<S>` 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<S> {
kind: PropertyError,
serializer: Serializer<Struct<S>>,
}
impl<S> Serializer<Struct<S>> {
fn serialize_property(
self,
name: &str,
) -> Result<Serializer<Property<Struct<S>>>, PropertySerializeError<S>> {
/* ... */
}
}
```
- 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.
</details>