1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-08-08 16:26:35 +02:00

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
This commit is contained in:
Glen De Cauwsemaecker
2025-08-03 11:25:18 +02:00
parent 14cc136c3e
commit b61c3378c5
3 changed files with 101 additions and 132 deletions

View File

@ -439,6 +439,7 @@
- [Is It Encapsulated?](idiomatic/leveraging-the-type-system/newtype-pattern/is-it-encapsulated.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](idiomatic/leveraging-the-type-system/typestate-pattern.md)
- [Typestate Pattern Example](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-example.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) - [Typestate Pattern with Generics](idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md)
--- ---

View File

@ -0,0 +1,96 @@
## 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**.
`SerializeStructProperty` and `SerializeList` now share similar logic (e.g.
adding strings, nested structs, or nested lists).
- 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>

View File

@ -1,141 +1,13 @@
## Typestate Pattern with Generics ## Typestate Pattern with Generics
Generics can be used with the typestate pattern to reduce duplication and allow TODO
shared logic across state variants, while still encoding state transitions in
the type system.
```rust ```rust,editable
#[non_exhaustive] // TODO
struct Insecure;
struct Secure {
client_cert: Option<Vec<u8>>,
}
trait Transport {
/* ... */
}
impl Transport for Insecure {
/* ... */
}
impl Transport for Secure {
/* ... */
}
#[non_exhaustive]
struct WantsTransport;
struct Ready<T> {
transport: T,
}
struct ConnectionBuilder<T> {
host: String,
timeout: Option<u64>,
stage: T,
}
struct Connection {/* ... */}
impl Connection {
fn new(host: &str) -> ConnectionBuilder<WantsTransport> {
ConnectionBuilder {
host: host.to_owned(),
timeout: None,
stage: WantsTransport,
}
}
}
impl<T> ConnectionBuilder<T> {
fn timeout(mut self, secs: u64) -> Self {
self.timeout = Some(secs);
self
}
}
impl ConnectionBuilder<WantsTransport> {
fn insecure(self) -> ConnectionBuilder<Ready<Insecure>> {
ConnectionBuilder {
host: self.host,
timeout: self.timeout,
stage: Ready { transport: Insecure },
}
}
fn secure(self) -> ConnectionBuilder<Ready<Secure>> {
ConnectionBuilder {
host: self.host,
timeout: self.timeout,
stage: Ready { transport: Secure { client_cert: None } },
}
}
}
impl ConnectionBuilder<Ready<Secure>> {
fn client_certificate(mut self, raw: Vec<u8>) -> Self {
self.stage.transport.client_cert = Some(raw);
self
}
}
impl<T: Transport> ConnectionBuilder<Ready<T>> {
fn connect(self) -> std::io::Result<Connection> {
// ... use valid state to establish the configured connection
Ok(Connection {})
}
}
fn main() -> std::io::Result<()> {
let _conn = Connection::new("db.local")
.secure()
.client_certificate(vec![1, 2, 3])
.timeout(10)
.connect()?;
Ok(())
}
``` ```
<details> <details>
- This example extends the typestate pattern using **generic parameters** to - TODO
avoid duplication of common logic.
- We use a generic type `T` to represent the current stage of the builder, and
share fields like `host` and `timeout` across all stages.
- The transport phase uses `insecure()` and `secure()` to transition from
`WantsTransport` into `Ready<T>`, where `T` is a type that implements the
`Transport` trait.
- Only once the connection is in a `Ready<T>` state, we can call `.connect()`,
guaranteed at compile time.
- Using generics allows us to avoid writing separate `BuilderForSecure`,
`BuilderForInsecure`, etc. structs.
Shared behavior, like `.timeout(...)`, can be implemented once and reused
across all states.
- This same design appears
[in real-world libraries like **Rustls**](https://docs.rs/rustls/latest/rustls/struct.ConfigBuilder.html),
where the `ConfigBuilder` uses typestate and generics to guide users through a
safe, ordered configuration flow.
It enforces at compile time that users must choose protocol versions, a
certificate verifier, and client certificate options, in the correct sequence,
before building a config.
- **Downsides** of this approach include:
- The documentation of the various builder types can become difficult to
follow, since their names are generated by generics and internal structs
like `Ready<T>`.
- Error messages from the compiler may become more opaque, especially if a
trait bound is not satisfied or a state transition is incomplete.
The error messages might also be hard to follow due to the complexity as a
result of the nested generics types.
- Still, in return for this complexity, you get compile-time enforcement of
valid configuration, clear builder sequencing, and no possibility of
forgetting a required step or misusing the API at runtime.
</details> </details>