Comment by throwaway894345
6 hours ago
I’ve been experimenting with Rust’s type state pattern—I’m trying to build something that builds an inventory of some object storage prefix (recording the version and size of each object in the prefix), but the pattern seemed so cumbersome. The goal was to avoid committing to a particular I/O color (sync vs async) and to have a testable no_std core, but I have so much less confidence in the typestate version compared to the “define traits for I/O and build an imperative loop around it”. I’m curious if anyone has suggestions (I realize it’s probably difficult to help without access to source code).
Why is it cumbersome?
The article also mentions that typestates can be cumbersome:
> Typestate improves code faultlessness and testability, but comes at the cost of more boilerplate code and can degrade readability.
I have noticed this in my own code. `Ticket` with an internal variable tracking the state makes using it simpler. I just have to store one object in my struct `struct MyData { ticket: Ticket }` and call `ticket` methods in the correct order.
Typestate `Ticket<T>` is not as simple. I have to wrap it in my own enum: `enum TicketState { Ticket1(Ticket<Func1Done>), Ticket2(Ticket<Func2Done>), }` to store in my struct: `struct MyData { ticket: TicketState }`. Then every time I call `ticket` methods, I must extract the correct variant value first. That degrades readability and creates extra run-time cost.
You don't need the enum? You just require Ticket<T_0> as a function argument.
It's really not that cumbersome, it's like two extra lines of code...
I’m probably doing it wrong, but when there’s a state with multiple transitions out, I can either model it as distinct methods per transition in which case the caller needs to know how to transition between states or I can have the caller pass an enum in which moves the branch into the state machine at the expense of an enum and a match statement. It’s also like 10x the code. Again, I’m very open to the possibility that I’m doing something wrong. Curious how you would model a state machine for (1) reserving the right to do the inventory (2) querying the next page of results (based on a cursor) and (3) recording the page information and the next cursor.
In the type state pattern you would have something like this
pub trait ValidState {}
struct StateMachine<'a, T>
where
{
untyped: &'a mut UntypedStateMachine,
_marker: PhantomData<T>
}
fn reserve_right<'a>(state: StateMachine<'a, Begin>) -> StateMachine<'a, Reserved>
fn query<'a>(state: StateMachine<'a, Reserved>) -> StateMachine<'a, Queried>
fn record<'a>(state: StateMachine<'a, Queried>) -> StateMachine<'a, Recorded>