Skip to content

Commit b61c337

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 b61c337

File tree

3 files changed

+101
-132
lines changed

3 files changed

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