Thanks again for another option to tackle the problem. That type state pattern definitely makes sense in some cases, however, in other's I'm undecided if it feels more like a better solution or just a workaround.
Let me get the problem more specific in two steps:
Step 1: Cases where I tripped over the situation are mainly cases where I want to implement what I'd call "lazy, cached getters": The struct is able to provide some data via a getter method but as that data is not always needed and costly to retrieve it's not necessarily present already and the getter method might need to retrieve it first (and it might need to mutate self for that). On the other hand, if the struct gets asked for the data multiple times, it should not (and might not be able to) retrieve it again, so the first call caches the data. A naive approach to that caching already requires a mutable self. Of course I can avoid that by using e. g. a (non-std) LazyCell which actually fits the situation pretty good but I might still need a mutable self when retrieving the data for the first time.
Step 2: One example I'm playing with is a struct that represents an HTTP response. It shall have such getters for e. g. the body, the encoding, and the text. Note that those attributes also depend on each other: To get the decoded text (if it's not already known), we first need the binary body and the encoding. The body can be retrieved by reading an internally stored stream to its end. A mutable reference to self is needed for that read operation. Of course I could avoid that by using another Cell-ish data structure that holds the stream but wrapping everything with Cells just to avoid mutable references to self seems odd.
Translating the type state pattern to that example would yield something like:
Response<Initial> -> Response<Read> -> Response<ReadAndEncodingKnown> -> Response<Decoded>
At first sight that seems rather verbose as one would have to call something like read().detect_encoding().decode().text()
instead of just text()
, but of course Response<Initial>
could have shortcut methods for detect_encoding
, decode
, and text
that do the necessary calls internally.
What bugs me is that the graph could get rather large and complex if we're dealing with many different properties. Even with the few from the example, we could go a different way as there's no need to read the body to get the encoding (the header is already known initially), so we'd also have two other transitions: Response<Initial> -> Response<EncodingKnown> -> Response<ReadAndEncodingKnown>
.
(I wonder if there's a Crate that deals with all the boilerplate code for that pattern.)
Looking forward to read your thoughts.