Beyond Simple Typestate
How do we manage increasingly complex configuration flows with many possible states and transitions, while still preventing incompatible operations?
#![allow(unused)] fn main() { 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) -> ??? } }
Diagram of valid transitions:
-
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). -
From the diagram of valid transitions, 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.