This is precisely the problem addressed by typestates and session types. Each state has a unique type, and in that state, one can only call the appropriate functions.
Rust's affine typing solves precisely this problem as well. Your state change methods consume the previous state variable, making it a compile-time error to access it afterwards. Given code like in the article and assuming connect_unauthenticated takes self by value:
let i1 = socks5_socket();
let i2 = i1.connect_unauthenticated(proxy_addr);
let i3 = i1.connect_tcp(addr);
Rust will complain:
error[E0382]: use of moved value: `i1`
|
2 | let i2 = i1.connect_unauthenticated(proxy_addr);
| - value moved here
3 | let i3 = i1.connect_tcp(addr);
| ^ value used here after move
|
= note: move occurs because `i1` has type `socks5_socket`, which does not implement the `Copy` trait
Yes, between traits and alias control, you have everything required to solve OP's problem safely and efficiently. The only remaining aspect, I feel, is aesthetic, as the errors show; they show aliasing/ownership errors, but not indicative of operations on a single state machine.
Rust adds the constraint that any (non-copyable,non-dropable) variable has to be passed to a function (or operator, or return statement) exactly once:
Noncopyable a = ...;
Copyable b = ...;
f(a,b); //=> f(a,copy(&b)) // a is no longer in scope after this
g(b); // this is fine, we took a reference to b and made a copy without changing the original
h(a); // error: a is no longer a live variable
Er, yes; that's what I said (or rather, hanging various things including but not limited to pointers). C only supported the typechecking part of this because it doesn't have any facilities for compile-time linearity checking.
Linearity[0] and dependency[1] checking is what makes Rust a improvement over C, rather than yet another "let's reinvent C++, but slightly less awful" project.
0: "error: no implementation for copy〈T〉","error: no implementation for drop〈T〉"[2]
1: "error: cannot move/destroy x (of type T); y (of type T&'x) outlives it"
> What is wrong with implementing the states with polymorphic classes providing the API or an interface to it?
Yeah that's pretty much the basic approach to this problem: use a strategy pattern in all states of the state machine, and just provide definitions for each method in the state that the method makes sense.
That's pretty much how HTTP-based protocols are implemented when status codes drive a state machine.
I wonder why the author decided to ignore the most basic and simple solutions to a frequent problem in favour of a convoluted solution that doesn't add anything and is harder to debug.
Interestingly, Rust used to have typestates in its very early incarnation. See: https://pcwalton.github.io/2012/12/26/typestate-is-dead.html