You've already forked comprehensive-rust
mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-08-08 08:22:52 +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:
@ -439,6 +439,7 @@
|
||||
- [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)
|
||||
|
||||
---
|
||||
|
@ -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>
|
@ -1,141 +1,13 @@
|
||||
## Typestate Pattern with Generics
|
||||
|
||||
Generics can be used with the typestate pattern to reduce duplication and allow
|
||||
shared logic across state variants, while still encoding state transitions in
|
||||
the type system.
|
||||
TODO
|
||||
|
||||
```rust
|
||||
#[non_exhaustive]
|
||||
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(())
|
||||
}
|
||||
```rust,editable
|
||||
// TODO
|
||||
```
|
||||
|
||||
<details>
|
||||
|
||||
- This example extends the typestate pattern using **generic parameters** to
|
||||
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.
|
||||
- TODO
|
||||
|
||||
</details>
|
||||
|
Reference in New Issue
Block a user