Skip to content

Commit 14cc136

Browse files
committed
rework the initial typestate no-generic content
1 parent 4b0870e commit 14cc136

File tree

3 files changed

+153
-60
lines changed

3 files changed

+153
-60
lines changed

src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,7 @@
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)
440440
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
441+
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
441442
- [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)
442443

443444
---
Lines changed: 54 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,88 @@
11
---
2-
minutes: 15
2+
minutes: 30
33
---
44

5-
## Typestate Pattern
5+
## Typestate Pattern: Problem
66

7-
Typestate is the practice of encoding a part of the state of the value in its type, preventing incorrect or inapplicable operations from being called on the value.
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 _;
812
9-
```rust
10-
# use std::fmt::Write;
1113
#[derive(Default)]
12-
struct Serializer { output: String }
13-
struct SerializeStruct { serializer: Serializer }
14+
struct Serializer {
15+
output: String,
16+
}
1417
1518
impl Serializer {
16-
fn serialize_struct(mut self, name: &str) -> SerializeStruct {
19+
fn serialize_struct_start(&mut self, name: &str) {
1720
let _ = writeln!(&mut self.output, "{name} {{");
18-
SerializeStruct { serializer: self }
1921
}
20-
}
2122
22-
impl SerializeStruct {
23-
fn serialize_field(mut self, key: &str, value: &str) -> Self {
24-
let _ = writeln!(&mut self.serializer.output, " {key}={value};");
25-
self
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");
2629
}
2730
28-
fn finish_struct(mut self) -> Serializer {
29-
self.serializer.output.push_str("}\n");
30-
self.serializer
31+
fn finish(self) -> String {
32+
self.output
3133
}
3234
}
3335
3436
fn main() {
35-
let serializer = Serializer::default()
36-
.serialize_struct("User")
37-
.serialize_field("id", "42")
38-
.serialize_field("name", "Alice")
39-
.finish_struct();
40-
println!("{}", serializer.output);
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());
4145
}
4246
```
4347

4448
<details>
4549

46-
- This example is inspired by
47-
[Serde's `Serializer` trait](https://docs.rs/serde/latest/serde/ser/trait.Serializer.html).
48-
For a deeper explanation of how Serde models serialization as a state machine,
49-
see <https://serde.rs/impl-serializer.html>.
50-
51-
- The typestate pattern allows us to model state machines using Rust’s type
52-
system. In this case, the state machine is a simple serializer.
53-
54-
- The key idea is that at each state in the process, we can only
55-
do the actions which are valid for that state. Transitions between
56-
states happen by consuming one value and producing another.
50+
- This `Serializer` is meant to write a structured value. The expected usage
51+
follows this sequence:
5752

5853
```bob
59-
+------------+ serialize struct +-----------------+
60-
| Serializer +-------------------->| SerializeStruct |<-------+
61-
+------------+ +-+-----+---------+ |
62-
^ | | |
63-
| finish struct | | serialize field |
64-
+-----------------------------+ +------------------+
54+
serialize struct start
55+
-+---------------------
56+
|
57+
+--> serialize struct field
58+
-+---------------------
59+
|
60+
+--> serialize struct field
61+
-+---------------------
62+
|
63+
+--> serialize struct end
6564
```
6665

67-
- In the example above:
68-
69-
- Once we begin serializing a struct, the `Serializer` is moved into the
70-
`SerializeStruct` state. At that point, we no longer have access to the
71-
original `Serializer`.
66+
- However, in this example we forgot to call `serialize_struct_end()` before
67+
`finish()`. As a result, the serialized output is incomplete or syntactically
68+
incorrect.
7269

73-
- While in the `SerializeStruct` state, we can only call methods related to
74-
writing fields. We cannot use the same instance to serialize a tuple, list,
75-
or primitive. Those constructors simply do not exist here.
70+
- One approach to fix this would be to track internal state manually, and return
71+
a `Result` from methods like `serialize_struct_field()` or `finish()` if the
72+
current state is invalid.
7673

77-
- Only after calling `finish_struct` do we get the `Serializer` back. At that
78-
point, we can inspect the output or start a new serialization session.
74+
- But this has downsides:
7975

80-
- If we forget to call `finish_struct` and drop the `SerializeStruct` instead,
81-
the original `Serializer` is lost. This ensures that incomplete or invalid
82-
output can never be observed.
76+
- It is easy to get wrong as an implementer. Rust’s type system cannot help
77+
enforce the correctness of our state transitions.
8378

84-
- By contrast, if all methods were defined on `Serializer` itself, nothing would
85-
prevent users from mixing serialization modes or leaving a struct unfinished.
79+
- It also adds unnecessary burden on the user, who must handle `Result` values
80+
for operations that are misused in source code rather than at runtime.
8681

87-
- This pattern avoids such misuse by making it **impossible to represent invalid
88-
transitions**.
82+
- A better solution is to model the valid state transitions directly in the type
83+
system.
8984

90-
- One downside of typestate modeling is potential code duplication between
91-
states. In the next section, we will see how to use **generics** to reduce
92-
duplication while preserving correctness.
85+
In the next slide, we will apply the **typestate pattern** to enforce correct
86+
usage at compile time and make invalid states unrepresentable.
9387

9488
</details>
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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+
let _ = writeln!(&mut self.output, "{name} {{");
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+
let _ = writeln!(&mut self.serializer.output, " {key}={value};");
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+
<details>
53+
54+
- This example is inspired by Serde’s
55+
[`Serializer` trait](https://docs.rs/serde/latest/serde/ser/trait.Serializer.html).
56+
Serde uses typestates internally to ensure serialization follows a valid
57+
structure. For more, see: <https://serde.rs/impl-serializer.html>
58+
59+
- The key idea behind typestate is that state transitions happen by consuming a
60+
value and producing a new one. At each step, only operations valid for that
61+
state are available.
62+
63+
```bob
64+
+------------+ serialize struct +-----------------+
65+
| Serializer +-------------------->| SerializeStruct |<-------+
66+
+--+---------+ +-+-----+---------+ |
67+
| ^ | | |
68+
| | finish struct | | serialize field |
69+
| +-----------------------------+ +------------------+
70+
|
71+
+---> finish
72+
```
73+
74+
- In this example:
75+
76+
- We begin with a `Serializer`, which only allows us to start serializing a
77+
struct.
78+
79+
- Once we call `.serialize_struct(...)`, ownership moves into a
80+
`SerializeStruct` value. From that point on, we can only call methods
81+
related to serializing struct fields.
82+
83+
- The original `Serializer` is no longer accessible — preventing us from
84+
mixing modes (like writing a tuple or primitive mid-struct) or calling
85+
`finish()` too early.
86+
87+
- Only after calling `.finish_struct()` do we receive the `Serializer` back.
88+
At that point, the output can be finalized or reused.
89+
90+
- If we forget to call `finish_struct()` and drop the `SerializeStruct` early,
91+
the `Serializer` is also dropped. This ensures incomplete output cannot leak
92+
into the system.
93+
94+
- By contrast, if we had implemented everything on `Serializer` directly — as
95+
seen on the previous slide, nothing would stop someone from skipping important
96+
steps or mixing serialization flows.
97+
98+
</details>

0 commit comments

Comments
 (0)