Skip to content

Commit d727cd7

Browse files
committed
write new draft of typestate advanced intro
this is again in the flow of a problem statement first, building on our original example, and in next slide we'll add the solution with generics
1 parent 14cc136 commit d727cd7

File tree

3 files changed

+104
-132
lines changed

3 files changed

+104
-132
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@
439439
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
440440
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
441441
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
442+
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
442443
- [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)
443444

444445
---
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
## Beyond Simple Typestate
2+
3+
How do we manage increasingly complex configuration flows with many possible
4+
states and transitions, while still preventing incompatible operations?
5+
6+
```rust,editable
7+
struct Serializer {/* [...] */}
8+
struct SerializeStruct {/* [...] */}
9+
struct SerializeStructProperty {/* [...] */}
10+
struct SerializeList {/* [...] */}
11+
12+
impl Serializer {
13+
// TODO:
14+
// Begin serializing a top-level struct or property.
15+
//
16+
// fn serialize_property(mut self, name: &str) -> SerializeStructProperty
17+
}
18+
19+
impl SerializeStruct {
20+
// TODO:
21+
// Add a named property to this struct.
22+
//
23+
// fn serialize_property(mut self, name: &str) -> SerializeStructProperty
24+
25+
// TODO:
26+
// How should we finish this struct? This depends on where it appears:
27+
// - At the root level: return `Serializer`
28+
// - As a property inside another struct: return `SerializeStruct`
29+
// - As a value inside a list: return `SerializeList`
30+
//
31+
// fn finish(mut self) -> ???
32+
}
33+
34+
impl SerializeStructProperty {
35+
// TODO:
36+
// Serialize the value for the current property.
37+
//
38+
// fn serialize_string(mut self, value: &str) -> SerializeStruct
39+
// fn serialize_struct(mut self, name: &str) -> SerializeStruct
40+
// fn serialize_list(mut self) -> SerializeList
41+
// fn finish(mut self) -> SerializeStruct
42+
}
43+
44+
impl SerializeList {
45+
// TODO:
46+
// Serialize a value into the list.
47+
//
48+
// fn serialize_string(mut self, value: &str) -> Self
49+
// fn serialize_struct(mut self, value: &str) -> SerializeStruct
50+
// fn serialize_list(mut self) -> SerializeList
51+
52+
// TODO:
53+
// Like `SerializeStruct::finish`, the return type depends on nesting.
54+
//
55+
// fn finish(mut self) -> ???
56+
}
57+
```
58+
59+
<details>
60+
61+
- Building on our previous serializer, we now want to support **nested
62+
structures** and **lists**.
63+
64+
- However, this introduces both **duplication** and **structural complexity**.
65+
66+
`SerializeStructProperty` and `SerializeList` now share similar logic (e.g.
67+
adding strings, nested structs, or nested lists).
68+
69+
- Even more critically, we now hit a **type system limitation**: we cannot
70+
cleanly express what `finish()` should return without duplicating variants for
71+
every nesting context (e.g. root, struct, list).
72+
73+
- To better understand this limitation, let’s map the valid transitions:
74+
75+
```bob
76+
+-----------+ +---------+------------+-----+
77+
| | | | | |
78+
V | V | V |
79+
+ |
80+
serializer --> structure --> property --> list +-+
81+
82+
| ^ |
83+
V | |
84+
+-----------+
85+
String
86+
```
87+
88+
- From this diagram, we can observe:
89+
- The transitions are recursive
90+
- The return types depend on _where_ a substructure or list appears
91+
- Each context requires a return path to its parent
92+
93+
- With only concrete types, this becomes unmanageable. Our current approach
94+
leads to an explosion of types and manual wiring.
95+
96+
- In the next chapter, we’ll see how **generics** let us model recursive flows
97+
with less boilerplate, while still enforcing valid operations at compile time.
98+
99+
</details>
Lines changed: 4 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,13 @@
11
## Typestate Pattern with Generics
22

3-
Generics can be used with the typestate pattern to reduce duplication and allow
4-
shared logic across state variants, while still encoding state transitions in
5-
the type system.
3+
TODO
64

7-
```rust
8-
#[non_exhaustive]
9-
struct Insecure;
10-
struct Secure {
11-
client_cert: Option<Vec<u8>>,
12-
}
13-
14-
trait Transport {
15-
/* ... */
16-
}
17-
impl Transport for Insecure {
18-
/* ... */
19-
}
20-
impl Transport for Secure {
21-
/* ... */
22-
}
23-
24-
#[non_exhaustive]
25-
struct WantsTransport;
26-
struct Ready<T> {
27-
transport: T,
28-
}
29-
30-
struct ConnectionBuilder<T> {
31-
host: String,
32-
timeout: Option<u64>,
33-
stage: T,
34-
}
35-
36-
struct Connection {/* ... */}
37-
38-
impl Connection {
39-
fn new(host: &str) -> ConnectionBuilder<WantsTransport> {
40-
ConnectionBuilder {
41-
host: host.to_owned(),
42-
timeout: None,
43-
stage: WantsTransport,
44-
}
45-
}
46-
}
47-
48-
impl<T> ConnectionBuilder<T> {
49-
fn timeout(mut self, secs: u64) -> Self {
50-
self.timeout = Some(secs);
51-
self
52-
}
53-
}
54-
55-
impl ConnectionBuilder<WantsTransport> {
56-
fn insecure(self) -> ConnectionBuilder<Ready<Insecure>> {
57-
ConnectionBuilder {
58-
host: self.host,
59-
timeout: self.timeout,
60-
stage: Ready { transport: Insecure },
61-
}
62-
}
63-
64-
fn secure(self) -> ConnectionBuilder<Ready<Secure>> {
65-
ConnectionBuilder {
66-
host: self.host,
67-
timeout: self.timeout,
68-
stage: Ready { transport: Secure { client_cert: None } },
69-
}
70-
}
71-
}
72-
73-
impl ConnectionBuilder<Ready<Secure>> {
74-
fn client_certificate(mut self, raw: Vec<u8>) -> Self {
75-
self.stage.transport.client_cert = Some(raw);
76-
self
77-
}
78-
}
79-
80-
impl<T: Transport> ConnectionBuilder<Ready<T>> {
81-
fn connect(self) -> std::io::Result<Connection> {
82-
// ... use valid state to establish the configured connection
83-
Ok(Connection {})
84-
}
85-
}
86-
87-
fn main() -> std::io::Result<()> {
88-
let _conn = Connection::new("db.local")
89-
.secure()
90-
.client_certificate(vec![1, 2, 3])
91-
.timeout(10)
92-
.connect()?;
93-
Ok(())
94-
}
5+
```rust,editable
6+
// TODO
957
```
968

979
<details>
9810

99-
- This example extends the typestate pattern using **generic parameters** to
100-
avoid duplication of common logic.
101-
102-
- We use a generic type `T` to represent the current stage of the builder, and
103-
share fields like `host` and `timeout` across all stages.
104-
105-
- The transport phase uses `insecure()` and `secure()` to transition from
106-
`WantsTransport` into `Ready<T>`, where `T` is a type that implements the
107-
`Transport` trait.
108-
109-
- Only once the connection is in a `Ready<T>` state, we can call `.connect()`,
110-
guaranteed at compile time.
111-
112-
- Using generics allows us to avoid writing separate `BuilderForSecure`,
113-
`BuilderForInsecure`, etc. structs.
114-
115-
Shared behavior, like `.timeout(...)`, can be implemented once and reused
116-
across all states.
117-
118-
- This same design appears
119-
[in real-world libraries like **Rustls**](https://docs.rs/rustls/latest/rustls/struct.ConfigBuilder.html),
120-
where the `ConfigBuilder` uses typestate and generics to guide users through a
121-
safe, ordered configuration flow.
122-
123-
It enforces at compile time that users must choose protocol versions, a
124-
certificate verifier, and client certificate options, in the correct sequence,
125-
before building a config.
126-
127-
- **Downsides** of this approach include:
128-
- The documentation of the various builder types can become difficult to
129-
follow, since their names are generated by generics and internal structs
130-
like `Ready<T>`.
131-
- Error messages from the compiler may become more opaque, especially if a
132-
trait bound is not satisfied or a state transition is incomplete.
133-
134-
The error messages might also be hard to follow due to the complexity as a
135-
result of the nested generics types.
136-
137-
- Still, in return for this complexity, you get compile-time enforcement of
138-
valid configuration, clear builder sequencing, and no possibility of
139-
forgetting a required step or misusing the API at runtime.
11+
- TODO
14012

14113
</details>

0 commit comments

Comments
 (0)