6 ms·
State Machines and the Strange Case of Mutating API
- xaedes 8y ago> What options do we have to support such mutating API at the moment? What is wrong with implementing the states with polymorphic classes providing the API or an interface to it?
- rumcajz 8y agoCan you give an example?
- sovande 8y agoApple's GameplayKit has several examples https://developer.apple.com/library/archive/documentation/General/Conceptual/GameplayKit_Guide/StateMachine.html https://developer.apple.com/library/archive/documentation/Ge...
- rahulmehta95 8y agoI was thinking that using Visitors would be a solution, which is a form of implementing a polymorphic API.
- geezerjay 8y ago> 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.
- sriram_malhar 8y agoThis 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. Interestingly, Rust used to have typestates in its very early incarnation. See: https://pcwalton.github.io/2012/12/26/typestate-is-dead.html https://pcwalton.github.io/2012/12/26/typestate-is-dead.html
- Sharlin 8y agoRust'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
- goldenkey 8y agoThat is really marvelous. How is this kind of thing implemented under the hood? That new wizkid language Zig by Andrew Kelly - I wonder if it has anything similar?
- a1369209993 8y agoYou can actually do the type-checking part of this even in C, it's just uglier: struct sock5_blank i1 = socks5_socket(); struct sock5_authed i2 = connect_unauthenticated(i1,proxy_addr); struct sock5_tcp i3 = connect_tcp(i1,addr); /* error: expected struct sock5_authed, got struct sock5_blank */ 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
- ppppppaul 8y agoproblem solved via pattern matching (visitor pattern in oop).
- typedef_struct 8y agoI'd probably do it like this: socks5_socket([&](auto closed_socket) { closed_socket.connect_unauthenticated(proxy_addr, [&](auto authenticated_socket) { authenticated_socket.connect_tcp(addr, [&](auto tcp_socket) { //tcp_socket.send or w/e }); }); });