Live data from Hacker News

State Machines in Rust

blog.yoshuawuyts.com

61–70 of 119 posts

Re: State Machines in Rust

#61
post #17
post #7

Earlier quoted context omitted.

The cited post[1] recommends an enum for this job, which avoids the dynamic dispatch and makes it easier to get at a specific state’s data when you have the whole machine. [1] https://hoverbear.org/blog/rust-state-machine-pattern/

I am not a fan of the hoverbear state machine pattern. I find that it makes simple things complicated and hard things impossible. I used it and I had problems with: - Reusing code between states. - Making callbacks to other APIs during state changes. I ended up using the standard state pattern described here: https://doc.rust-lang.org/book/ch17-03-oo-design-patterns.ht... The state pattern is not considered to be idi…

for reusing code between states, did you try impl blocks that are generic over the state type parameter?

for

    struct State { state: T }
with possible states

    struct A { id: Uuid }
    struct B { id: Uuid }
use a trait

    trait HasId {
        fn id_mut(&mut self) -> &mut Uuid;
    }
now you can impl over both A and B

    impl for State {
        fn new_id(&mut self) { *self.state.id_mut() = Uuid::new_v4() }
    }
simplistic example but enough to communicate the idea hopefully.

generally, macros makes this kind of thing ergonomic so you can generate the trait implementations and so on. otherwise it's a lot of typing.

don't understand what you mean about making callbacks to other APIs during transitions.

Re: State Machines in Rust

#62
You can invert the box; instead of putting the state inside the struct, put the struct inside the state. So instead of having a Foo with a Yellow state, have a Yellow Foo. You can then call methods on the Yellow state to get a Foo in the next state.

To make it work better, you'd need type-level functions, or some other kind of compile-time function, which can be called when instantiating something with compile-time arguments (like generics). Anything less and you lose the static checking when try and treat your state as a first-class value. The computation of the type (state) needs to happen at compile time. Otherwise you're just back in dynamic land and you might as well put in assertions.

Re: State Machines in Rust

#63
I’ve really enjoyed writing state machines in Swift. The rich enums allow states to be expressed with wrapped data (associated values) and events to also be expressed as rich enums.

I’ve begun using the pattern to represent state in views, navigation, and obviously when modeling processes (like a firmware update or Bluetooth connection).

Re: State Machines in Rust

#64
post #60

Noob question but what about state machines where a given state could transition to more than one other state depending on some outside factors? Or is that no longer considered a state machine? For a relevant to me example, a VM state. A VM in running state could be transitioned to terminated or stopped or hibernating depending on an admins action.

that's known as an NFA (Nondeterministic Finite Automaton), a variant of FSMs.

[deleted]

Re: State Machines in Rust

#65
post #58

Noob question but what about state machines where a given state could transition to more than one other state depending on some outside factors? Or is that no longer considered a state machine? For a relevant to me example, a VM state. A VM in running state could be transitioned to terminated or stopped or hibernating depending on an admins action.

You might queue up events which cause it to transition to another state. If you hit the hibernate button, it might finish rendering the current frame before checking to see if the button was pressed, then hibernate. So it's the same state machine just with a larger input space.

Sure but how does that work with the provided implementation where all states can only transition to a single state, this is ensured at compile time. What does the code look like that allows a state to transition to one of several other states?

Re: State Machines in Rust

#66
post #58

Earlier quoted context omitted.

You might queue up events which cause it to transition to another state. If you hit the hibernate button, it might finish rendering the current frame before checking to see if the button was pressed, then hibernate. So it's the same state machine just with a larger input space.

Sure but how does that work with the provided implementation where all states can only transition to a single state, this is ensured at compile time. What does the code look like that allows a state to transition to one of several other states?

No Rust, but here's a Python implementation that I have built on top of before: https://github.com/pytransitions/transitions

You add the concept of finite "triggers", where [state i] + [trigger result j] always takes you to [new state](which could be the same state if you want)

Triggers are just functions where anything could be happening - coin flip, API call, but they return one of an enumerated set of results so the machine can always use their result to go to another state.

Re: State Machines in Rust

#67
post #15

Advocating for programmer ergonomics is always a good thing. And I think more people should advocate and try to design languages in such a way that the way of programming more closely resembles the actual real thing [1, 2]. As you might recall in cognitive psychology there's a specific idea that translating a problem to a more recognizable problem (or simply changing the symbols) is a good thing [3]. Having less work…

While I love Bret's work I don't think that's scalable. If you can simulate time axis, it means it can simulate only systems that can are hundreds of times smaller than system resource. E.g. can you simulate a Kubernetes swarm with thousands of different settings? It's a great learning tool, but not much else.

Depends on the desired level of detail, and how "simulate-able" the system is designed to be. For example if everything uses a central event queue, one can to Discrete Event Simulation by just jumping to the next event. However an exiting Kubernetes swarm is not very simulate, as with most other existing software. And until (or if) simulation becomes a priority, this will continue to be the case.

Re: State Machines in Rust

#68
post #50

Earlier quoted context omitted.

Currently the compiler gives "not a type" when the brackets are removed; I agree that it might be to disambiguate, but it's confusing and redundant syntax. I expect { } to be used strictly for blocks in Rust.

It's a block though, isn't it? Color::Green is an enum variant while { Color::Green } is a (constant) block (expression) that evaluates to an instance which is used as a generic type parameter. The difference might be more easily understandable if we look at an enum variant that holds a value, where the syntactic differences between variant and instance constructor are more clearly visible. Color::RGB(u64, u64, u64)…

Hm, the RFC says the braces are needed if it is not an "identity expression" with the examples:

    const X: usize = 7;
    
    let x: RectangularArray;
    let y: RectangularArray;
So I'm still not sure if Color::Green is an identity expression, they say:

> Identity expression: An expression which cannot be evaluated further except by substituting it with names in scope. This includes all literals as well all idents

So... maybe? Depends on whether Color::Green is an ident or not. At the very least I'd expect this to work, but it doesn't yet:

    use Color::*;
    
    impl State { ... }

Re: State Machines in Rust

#69

Earlier quoted context omitted.

Sure but how does that work with the provided implementation where all states can only transition to a single state, this is ensured at compile time. What does the code look like that allows a state to transition to one of several other states?

No Rust, but here's a Python implementation that I have built on top of before: https://github.com/pytransitions/transitions You add the concept of finite "triggers", where [state i] + [trigger result j] always takes you to [new state](which could be the same state if you want) Triggers are just functions where anything could be happening - coin flip, API call, but they return one of an enumerated set of results so t…

Ah ok. I don't write Rust either but maybe it'd look like:

  impl State {
    pub fn next(self, Trigger) -> State {
        State { _inner: Hibernate {} }
    }

    pub fn next(self, Trigger) -> State {
        State { _inner: Terminate {} }
    }
  }

Re: State Machines in Rust

#70

Earlier quoted context omitted.

I believe they're intentional, to disambiguate from `mod Color { struct Green; }`, and because they can be arbitrarily complex expressions.

Currently the compiler gives "not a type" when the brackets are removed; I agree that it might be to disambiguate, but it's confusing and redundant syntax. I expect { } to be used strictly for blocks in Rust.

C++ requires using the template keyword to disambiguate, i.e., one has to write `foo.template bar2>()` as opposed to `foo.bar2>()` (did you mean `(foo.bar ()` ?).

The main reason Rust meta-programming is so much better than D and C++ is that Rust has an LL(k) grammar that's trivial to parse into ASTs that can be easily manipulated.

That feature alone is definitely worth the annoyance of having to use an editor that types `{}` for you when inputting a constant expression. It's the same with removing the `::` in `::` - can be done, but the costs are not worth the ergonomic improvement.

That is, I don't think the claim that the braces are redundant is correct - they are there for a reason: to keep a simple grammar, which happens to be one of the most important Rust features that everybody uses every day (every single proc macro uses this feature, that includes the `println!` in `println!("Hello World")` Rust examples).

Post reply on HN