As a separate point, I think this is an excellent example of making invalid states unrepresentable.
If you were to design Ikea furniture, you'd make pieces that only fit in to the total configuration the correct way.
Types provide that same phenomenon in programming imo. At the end of the day we are shoveling and playing with bytes so we need to provide handles to these processes which make sure that we can't fit a "square peg into a round hole"
> 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.
It's really not that cumbersome, it's like two extra lines of code...
pub trait ValidState {}
struct StateMachine<'a, T>
where
T: ValidState
{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>
Also, many other functions can depend on the ticket from func_1. So making the ticket separate and generic on the process is the right (imo) solution here.
On the other hand, doesnt seprating args and typestate defeat the purpose? Since they can now be constructed separately.
async fn write_buffer(buf: &mut [u8]) -> Ticket<BufferWritten>
//Best that it it's own function for readability
async fn complex_counter_logic(ctr: Arc<AtomicUsize>, ticket: Ticket<BufferWritten>) -> Ticket<ComplexCounterLogic>
//One could also place all the data in a giant struct and move that across all functions but that eventually leads to struct bloat unless we use an explicit state machine, in which case type state is better
Maybe I misunderstood something.
func1(foo_0) -> bar0
func2(foo_1, foo_2) -> bar1
func3(foo_3, foo_3) -> bar2
And you wanted to make sure that func2 and func3 can only be called after func1 has been called.
A wrapper on the output of func1 here would be awkward because then you return Wrapper<Func1Done>(bar0). But func2 does not even need a bar0 and neither does func3.
So the solution is to return (bar0, Wrapper<Func1Done>) from func1 where
struct Wrapper<T>(//cheating ())
Obviously if you are operating in a wide, concurrent async system then the Ticket and separate function calls is the better mechanism for the ordering.