Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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() {
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0

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:

structureserializerpropertylistString
  • 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.