From b61c3378c5ac7ba3a12557fc366c0932797e2b6a Mon Sep 17 00:00:00 2001 From: Glen De Cauwsemaecker Date: Sun, 3 Aug 2025 11:25:18 +0200 Subject: [PATCH] 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 --- src/SUMMARY.md | 1 + .../typestate-pattern/typestate-advanced.md | 96 +++++++++++++ .../typestate-pattern/typestate-generics.md | 136 +----------------- 3 files changed, 101 insertions(+), 132 deletions(-) create mode 100644 src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index fce36275..e09226bd 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -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) --- diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.md new file mode 100644 index 00000000..f16521e4 --- /dev/null +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-advanced.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) -> ??? +} +``` + +
+ +- 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. + +
diff --git a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md index b0e78c80..1ad01edc 100644 --- a/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md +++ b/src/idiomatic/leveraging-the-type-system/typestate-pattern/typestate-generics.md @@ -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>, -} - -trait Transport { - /* ... */ -} -impl Transport for Insecure { - /* ... */ -} -impl Transport for Secure { - /* ... */ -} - -#[non_exhaustive] -struct WantsTransport; -struct Ready { - transport: T, -} - -struct ConnectionBuilder { - host: String, - timeout: Option, - stage: T, -} - -struct Connection {/* ... */} - -impl Connection { - fn new(host: &str) -> ConnectionBuilder { - ConnectionBuilder { - host: host.to_owned(), - timeout: None, - stage: WantsTransport, - } - } -} - -impl ConnectionBuilder { - fn timeout(mut self, secs: u64) -> Self { - self.timeout = Some(secs); - self - } -} - -impl ConnectionBuilder { - fn insecure(self) -> ConnectionBuilder> { - ConnectionBuilder { - host: self.host, - timeout: self.timeout, - stage: Ready { transport: Insecure }, - } - } - - fn secure(self) -> ConnectionBuilder> { - ConnectionBuilder { - host: self.host, - timeout: self.timeout, - stage: Ready { transport: Secure { client_cert: None } }, - } - } -} - -impl ConnectionBuilder> { - fn client_certificate(mut self, raw: Vec) -> Self { - self.stage.transport.client_cert = Some(raw); - self - } -} - -impl ConnectionBuilder> { - fn connect(self) -> std::io::Result { - // ... 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 ```
-- 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`, where `T` is a type that implements the - `Transport` trait. - -- Only once the connection is in a `Ready` 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`. - - 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