You've already forked comprehensive-rust
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:
@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -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
|
## 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>
|
||||||
|
Reference in New Issue
Block a user