Skip to content
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@
- [Semantic Confusion](idiomatic/leveraging-the-type-system/newtype-pattern/semantic-confusion.md)
- [Parse, Don't Validate](idiomatic/leveraging-the-type-system/newtype-pattern/parse-don-t-validate.md)
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.md)
- [Typestate Pattern](idiomatic/leveraging-the-type-system/typestate-pattern.md)
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.md)
- [Beyond Simple Typestate](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md)
- [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)

---

Expand Down
89 changes: 89 additions & 0 deletions src/idiomatic/leveraging-the-type-system/typestate-pattern.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
minutes: 30
---

## Typestate Pattern: Problem

How can we ensure that only valid operations are allowed on a value based on its
current state?

```rust,editable
use std::fmt::Write as _;

#[derive(Default)]
struct Serializer {
output: String,
}

impl Serializer {
fn serialize_struct_start(&mut self, name: &str) {
let _ = writeln!(&mut self.output, "{name} {{");
}

fn serialize_struct_field(&mut self, key: &str, value: &str) {
let _ = writeln!(&mut self.output, " {key}={value};");
}

fn serialize_struct_end(&mut self) {
self.output.push_str("}\n");
}

fn finish(self) -> String {
self.output
}
}

fn main() {
let mut serializer = Serializer::default();
serializer.serialize_struct_start("User");
serializer.serialize_struct_field("id", "42");
serializer.serialize_struct_field("name", "Alice");

// serializer.serialize_struct_end(); // ← Oops! Forgotten

println!("{}", serializer.finish());
}
```

<details>

- This `Serializer` is meant to write a structured value. The expected usage
follows this sequence:

```bob
serialize struct start
-+---------------------
|
+--> serialize struct field
-+---------------------
|
+--> serialize struct field
-+---------------------
|
+--> serialize struct end
```

- However, in this example we forgot to call `serialize_struct_end()` before
`finish()`. As a result, the serialized output is incomplete or syntactically
incorrect.

- One approach to fix this would be to track internal state manually, and return
a `Result` from methods like `serialize_struct_field()` or `finish()` if the
current state is invalid.

- But this has downsides:

- It is easy to get wrong as an implementer. Rust’s type system cannot help
enforce the correctness of our state transitions.

- It also adds unnecessary burden on the user, who must handle `Result` values
for operations that are misused in source code rather than at runtime.

- A better solution is to model the valid state transitions directly in the type
system.

In the next slide, we will apply the **typestate pattern** to enforce correct
usage at compile time and make it impossible to call incompatible methods or
forget to do a required action.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## Beyond Simple Typestate

How do we manage increasingly complex configuration flows with many possible
states and transitions, while still preventing incompatible operations?

```rust
struct Serializer {/* [...] */}
struct SerializeStruct {/* [...] */}
struct SerializeStructProperty {/* [...] */}
struct SerializeList {/* [...] */}

impl Serializer {
// TODO, implement:
//
// fn serialize_struct(self, name: &str) -> SerializeStruct
// fn finish(self) -> String
}

impl SerializeStruct {
// TODO, implement:
//
// fn serialize_property(mut self, name: &str) -> SerializeStructProperty

// TODO,
// How should we finish this struct? This depends on where it appears:
// - At the root level: return `Serializer`
// - As a property inside another struct: return `SerializeStruct`
// - As a value inside a list: return `SerializeList`
//
// fn finish(self) -> ???
}

impl SerializeStructProperty {
// TODO, implement:
//
// fn serialize_string(self, value: &str) -> SerializeStruct
// fn serialize_struct(self, name: &str) -> SerializeStruct
// fn serialize_list(self) -> SerializeList
// fn finish(self) -> SerializeStruct
}

impl SerializeList {
// TODO, implement:
//
// fn serialize_string(mut self, value: &str) -> Self
// fn serialize_struct(mut self, value: &str) -> SerializeStruct
// fn serialize_list(mut self) -> SerializeList

// TODO:
// Like `SerializeStruct::finish`, the return type depends on nesting.
//
// fn finish(mut self) -> ???
}
```

<details>

- Building on our previous serializer, we now want to support **nested
structures** and **lists**.

- However, this introduces both **duplication** and **structural complexity**.

- Even more critically, we now hit a **type system limitation**: we cannot
cleanly express what `finish()` should return without duplicating variants for
every nesting context (e.g. root, struct, list).

- To better understand this limitation, let’s map the valid transitions:

```bob
+-----------+ +---------+------------+-----+
| | | | | |
V | V | V |
+ |
serializer --> structure --> property --> list +-+

| | ^ | ^
V | | | |
| +-----------+ |
String | |
+--------------------------+
```

- From this diagram, we can observe:
- The transitions are recursive
- The return types depend on _where_ a substructure or list appears
- Each context requires a return path to its parent

- With only concrete types, this becomes unmanageable. Our current approach
leads to an explosion of types and manual wiring.

- In the next chapter, we’ll see how **generics** let us model recursive flows
with less boilerplate, while still enforcing valid operations at compile time.

</details>
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
## Typestate Pattern: Example

The typestate pattern encodes part of a value’s runtime state into its type.
This allows us to prevent invalid or inapplicable operations at compile time.

```rust,editable
use std::fmt::Write as _;

#[derive(Default)]
struct Serializer {
output: String,
}

struct SerializeStruct {
serializer: Serializer,
}

impl Serializer {
fn serialize_struct(mut self, name: &str) -> SerializeStruct {
writeln!(&mut self.output, "{name} {{").unwrap();
SerializeStruct { serializer: self }
}

fn finish(self) -> String {
self.output
}
}

impl SerializeStruct {
fn serialize_field(mut self, key: &str, value: &str) -> Self {
writeln!(&mut self.serializer.output, " {key}={value};").unwrap();
self
}

fn finish_struct(mut self) -> Serializer {
self.serializer.output.push_str("}\n");
self.serializer
}
}

fn main() {
let serializer = Serializer::default()
.serialize_struct("User")
.serialize_field("id", "42")
.serialize_field("name", "Alice")
.finish_struct();

println!("{}", serializer.finish());
}
```

<details>

- This example is inspired by Serde’s
[`Serializer` trait](https://docs.rs/serde/latest/serde/ser/trait.Serializer.html).
Serde uses typestates internally to ensure serialization follows a valid
structure. For more, see: <https://serde.rs/impl-serializer.html>

- The key idea behind typestate is that state transitions happen by consuming a
value and producing a new one. At each step, only operations valid for that
state are available.

```bob
+------------+ serialize struct +-----------------+
| Serializer | ------------------> | SerializeStruct | <------+
+------------+ +-----------------+ |
|
| ^ | | |
| | finish struct | | serialize field |
| +-----------------------------+ +------------------+
|
+---> finish
```

- In this example:

- We begin with a `Serializer`, which only allows us to start serializing a
struct.

- Once we call `.serialize_struct(...)`, ownership moves into a
`SerializeStruct` value. From that point on, we can only call methods
related to serializing struct fields.

- The original `Serializer` is no longer accessible — preventing us from
mixing modes (such as starting another _struct_ mid-struct) or calling
`finish()` too early.

- Only after calling `.finish_struct()` do we receive the `Serializer` back.
At that point, the output can be finalized or reused.

- If we forget to call `finish_struct()` and drop the `SerializeStruct` early,
the `Serializer` is also dropped. This ensures incomplete output cannot leak
into the system.

- By contrast, if we had implemented everything on `Serializer` directly — as
seen on the previous slide, nothing would stop someone from skipping important
steps or mixing serialization flows.

</details>
Loading
Loading