Skip to content

Commit 63e50a8

Browse files
authored
add typestate pattern chapter for idiomatic rust (#2821)
1 parent a9497bd commit 63e50a8

File tree

10 files changed

+715
-0
lines changed

10 files changed

+715
-0
lines changed

src/SUMMARY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,14 @@
437437
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
438438
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
439439
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
440+
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
441+
- [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)
443+
- [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)
444+
- [Serializer: implement Root](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/root.md)
445+
- [Serializer: implement Struct](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/struct.md)
446+
- [Serializer: implement Property](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/property.md)
447+
- [Serializer: Complete implementation](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics/complete.md)
440448

441449
---
442450

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
minutes: 30
3+
---
4+
5+
## Typestate Pattern: Problem
6+
7+
How can we ensure that only valid operations are allowed on a value based on its
8+
current state?
9+
10+
```rust,editable
11+
use std::fmt::Write as _;
12+
13+
#[derive(Default)]
14+
struct Serializer {
15+
output: String,
16+
}
17+
18+
impl Serializer {
19+
fn serialize_struct_start(&mut self, name: &str) {
20+
let _ = writeln!(&mut self.output, "{name} {{");
21+
}
22+
23+
fn serialize_struct_field(&mut self, key: &str, value: &str) {
24+
let _ = writeln!(&mut self.output, " {key}={value};");
25+
}
26+
27+
fn serialize_struct_end(&mut self) {
28+
self.output.push_str("}\n");
29+
}
30+
31+
fn finish(self) -> String {
32+
self.output
33+
}
34+
}
35+
36+
fn main() {
37+
let mut serializer = Serializer::default();
38+
serializer.serialize_struct_start("User");
39+
serializer.serialize_struct_field("id", "42");
40+
serializer.serialize_struct_field("name", "Alice");
41+
42+
// serializer.serialize_struct_end(); // ← Oops! Forgotten
43+
44+
println!("{}", serializer.finish());
45+
}
46+
```
47+
48+
<details>
49+
50+
- This `Serializer` is meant to write a structured value.
51+
52+
- However, in this example we forgot to call `serialize_struct_end()` before
53+
`finish()`. As a result, the serialized output is incomplete or syntactically
54+
incorrect.
55+
56+
- One approach to fix this would be to track internal state manually, and return
57+
a `Result` from methods like `serialize_struct_field()` or `finish()` if the
58+
current state is invalid.
59+
60+
- But this has downsides:
61+
62+
- It is easy to get wrong as an implementer. Rust’s type system cannot help
63+
enforce the correctness of our state transitions.
64+
65+
- It also adds unnecessary burden on the user, who must handle `Result` values
66+
for operations that are misused in source code rather than at runtime.
67+
68+
- A better solution is to model the valid state transitions directly in the type
69+
system.
70+
71+
In the next slide, we will apply the **typestate pattern** to enforce correct
72+
usage at compile time and make it impossible to call incompatible methods or
73+
forget to do a required action.
74+
75+
</details>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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+
Diagram of valid transitions:
57+
58+
```bob
59+
+-----------+ +---------+------------+-----+
60+
| | | | | |
61+
V | V | V |
62+
+ |
63+
serializer --> structure --> property --> list +-+
64+
65+
| | ^ | ^
66+
V | | | |
67+
| +-----------+ |
68+
String | |
69+
+--------------------------+
70+
```
71+
72+
<details>
73+
74+
- Building on our previous serializer, we now want to support **nested
75+
structures** and **lists**.
76+
77+
- However, this introduces both **duplication** and **structural complexity**.
78+
79+
- Even more critically, we now hit a **type system limitation**: we cannot
80+
cleanly express what `finish()` should return without duplicating variants for
81+
every nesting context (e.g. root, struct, list).
82+
83+
- From the diagram of valid transitions, we can observe:
84+
- The transitions are recursive
85+
- The return types depend on _where_ a substructure or list appears
86+
- Each context requires a return path to its parent
87+
88+
- With only concrete types, this becomes unmanageable. Our current approach
89+
leads to an explosion of types and manual wiring.
90+
91+
- In the next chapter, we’ll see how **generics** let us model recursive flows
92+
with less boilerplate, while still enforcing valid operations at compile time.
93+
94+
</details>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
## Typestate Pattern: Example
2+
3+
The typestate pattern encodes part of a value’s runtime state into its type.
4+
This allows us to prevent invalid or inapplicable operations at compile time.
5+
6+
```rust,editable
7+
use std::fmt::Write as _;
8+
9+
#[derive(Default)]
10+
struct Serializer {
11+
output: String,
12+
}
13+
14+
struct SerializeStruct {
15+
serializer: Serializer,
16+
}
17+
18+
impl Serializer {
19+
fn serialize_struct(mut self, name: &str) -> SerializeStruct {
20+
writeln!(&mut self.output, "{name} {{").unwrap();
21+
SerializeStruct { serializer: self }
22+
}
23+
24+
fn finish(self) -> String {
25+
self.output
26+
}
27+
}
28+
29+
impl SerializeStruct {
30+
fn serialize_field(mut self, key: &str, value: &str) -> Self {
31+
writeln!(&mut self.serializer.output, " {key}={value};").unwrap();
32+
self
33+
}
34+
35+
fn finish_struct(mut self) -> Serializer {
36+
self.serializer.output.push_str("}\n");
37+
self.serializer
38+
}
39+
}
40+
41+
fn main() {
42+
let serializer = Serializer::default()
43+
.serialize_struct("User")
44+
.serialize_field("id", "42")
45+
.serialize_field("name", "Alice")
46+
.finish_struct();
47+
48+
println!("{}", serializer.finish());
49+
}
50+
```
51+
52+
`Serializer` usage flowchart:
53+
54+
```bob
55+
+------------+ serialize struct +-----------------+
56+
| Serializer | ------------------> | SerializeStruct | <------+
57+
+------------+ +-----------------+ |
58+
|
59+
| ^ | | |
60+
| | finish struct | | serialize field |
61+
| +-----------------------------+ +------------------+
62+
|
63+
+---> finish
64+
```
65+
66+
<details>
67+
68+
- This example is inspired by Serde’s
69+
[`Serializer` trait](https://docs.rs/serde/latest/serde/ser/trait.Serializer.html).
70+
Serde uses typestates internally to ensure serialization follows a valid
71+
structure. For more, see: <https://serde.rs/impl-serializer.html>
72+
73+
- The key idea behind typestate is that state transitions happen by consuming a
74+
value and producing a new one. At each step, only operations valid for that
75+
state are available.
76+
77+
- In this example:
78+
79+
- We begin with a `Serializer`, which only allows us to start serializing a
80+
struct.
81+
82+
- Once we call `.serialize_struct(...)`, ownership moves into a
83+
`SerializeStruct` value. From that point on, we can only call methods
84+
related to serializing struct fields.
85+
86+
- The original `Serializer` is no longer accessible — preventing us from
87+
mixing modes (such as starting another _struct_ mid-struct) or calling
88+
`finish()` too early.
89+
90+
- Only after calling `.finish_struct()` do we receive the `Serializer` back.
91+
At that point, the output can be finalized or reused.
92+
93+
- If we forget to call `finish_struct()` and drop the `SerializeStruct` early,
94+
the `Serializer` is also dropped. This ensures incomplete output cannot leak
95+
into the system.
96+
97+
- By contrast, if we had implemented everything on `Serializer` directly — as
98+
seen on the previous slide, nothing would stop someone from skipping important
99+
steps or mixing serialization flows.
100+
101+
</details>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Typestate Pattern with Generics
2+
3+
By combining typestate modeling with generics, we can express a wider range of
4+
valid states and transitions without duplicating logic. This approach is
5+
especially useful when the number of states grows or when multiple states share
6+
behavior but differ in structure.
7+
8+
```rust
9+
{{#include typestate-generics.rs:Serializer-def}}
10+
11+
{{#include typestate-generics.rs:Root-def}}
12+
{{#include typestate-generics.rs:Struct-def}}
13+
{{#include typestate-generics.rs:Property-def}}
14+
{{#include typestate-generics.rs:List-def}}
15+
```
16+
17+
We now have all the tools needed to implement the methods for the `Serializer`
18+
and its state type definitions. This ensures that our API only permits valid
19+
transitions, as illustrated in the following diagram:
20+
21+
Diagram of valid transitions:
22+
23+
```bob
24+
+-----------+ +---------+------------+-----+
25+
| | | | | |
26+
V | V | V |
27+
+ |
28+
serializer --> structure --> property --> list +-+
29+
30+
| | ^ | ^
31+
V | | | |
32+
| +-----------+ |
33+
String | |
34+
+--------------------------+
35+
```
36+
37+
<details>
38+
39+
- By leveraging generics to track the parent context, we can construct
40+
arbitrarily nested serializers that enforce valid transitions between struct,
41+
list, and property states.
42+
43+
- This enables us to build a recursive structure while maintaining strict
44+
control over which methods are accessible in each state.
45+
46+
- Methods common to all states can be defined for any `S` in `Serializer<S>`.
47+
48+
- Marker types (e.g., `List<S>`) introduce no memory or runtime overhead, as
49+
they contain no data other than a possible Zero-Sized Type. Their only role is
50+
to enforce correct API usage through the type system.
51+
52+
</details>

0 commit comments

Comments
 (0)