Live data from Hacker News

State Machines in Rust

blog.yoshuawuyts.com

91–100 of 119 posts

Re: State Machines in Rust

#91

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.

C++ requires using the template keyword to disambiguate, i.e., one has to write `foo.template bar 2>()` as opposed to `foo.bar 2>()` (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…

Removing redundancy is not always a good idea, but the braces are redundant.

Re: State Machines in Rust

#92

In my experience, state machines are very nice in theory, but in practice, over time, they devolve into a mess of spaghetti code. Because they are not in a single scope, loops become the equivalent of a bunch of gotos and managing lifetimes and locks becomes a problem because you can't use scope based mechanisms such as RAII. In Rust, if you want a state machine, generators are probably the long term way to go. https…

> Because they are not in a single scope

That all depends on the tool. Despite being cross-language (or rather, because of it), Ragel's source code-level interfaces don't require indirection through callbacks or function pointers. Ragel has a rich set of operators for embedding code blocks at transition points, operators for embedding expressions to control transitions, and operators for controlling how Ragel saves/restores state. You can organize your code nearly as freely as when open-coding a solution--a million little functions, a single gigantic function, or something in between.

The problem with many tools that are tightly integrated with a language--such as in-language AST manipulation, or via lambadas or closures--is that they're constrained by the expressiveness of the host language. You can see this with Lisp macros--you can trivially hack the s-expression tree to implement incredible semantic changes, but if the problem isn't best described using simple function (and specifically s-expression) syntax, then good luck identifying the semantics on inspection or understanding the implementation.

Re: State Machines in Rust

#93

C++20 supports enums as non-type template parameters, so I think it'd be possible to do it with enums there. Something like: enum class Color{Green, Yellow, Red}; template struct State{}; auto newState() -> State {...}; auto next(State ) -> State {...} auto next(State ) -> State {...} auto next(State ) -> State {...} int main(){ const auto state = newState(); // Green const auto state = next(state); // Yellow const a…

That example does not work, because declaring `state` multiple times creates an illegal C++ program (redeclaration of local variable - notice that this is not the case in Rust). You need to declare variables with different names: const auto state0 = newState(); const auto state1 = next(state0); const auto state2 = next(state1); const auto state3 = next(state); // TYPO -> BOOM use after move I don't think this can be…

Nobody is using state machines to advance several times through the states with variables named "stateN", so I am not sure what is the point.

There is no "BOOM" either since "use after move" is not a safety concern for those empty types, just a logic bug, which will likely appear at compile-time since your template specialization would not match your expectations.

The redeclaration in Rust always makes me uneasy as a default. It would have been better to require special syntax.

The rest about C++ users looks like flamebait to me.

Re: State Machines in Rust

#95
post #59

Earlier quoted context omitted.

the article includes a section on the state as a generic type parameter, though? in general, state as a type parameter is useful when there's some data that you want for every state (say, unique id, time of event), so those can be normal fields on the State struct, and then each event type can hold event-specific data. You can tie it together with From and TryFrom implementations that enable the specific transitions…

Associated types are slightly different than generic types. trait State { type NextState; fn transition(self) -> Self::NextState; } In this example one consumes the current state to yield the next state (one could use From/TryFrom, but I don't think that makes sense semantically, imo). The advantage of this approach is that if you design your State types such that they can only be constructed by transitioning from a…

You should keep going with this and see how ergonomic it is when NextState can be one of many states. I tried implementing State Machines once with this method and ended up wanting to use an `enum` for `State::NextState`. After a while it ended up being pretty unwieldy.

Re: State Machines in Rust

#97
Just to add more options, if you are willing to eat an allocation per transition, you can achieve a better developer experience by using trait objects.

You can define the trait:

    trait State: std::fmt::Debug {
        fn transition(&self) -> Option>;
    }
Then implement the trait for each struct, and transition:

    #[derive(Debug)]
    struct FirstState {
        foo: u32,
    }
    
    impl State for FirstState {
        fn transition(&self) -> Option> {
            println!("First State Hit {}", self.foo);
            Some(Box::new(SecondState {
                bar: "Hello".to_owned(),
            })) // transition to second state
        }
    }
Can even make an Iterator:

    struct StateIterator {
        curr_state: Option>,
    }
    
    impl StateIterator {
        fn new(curr_state: Option>) -> Self {
            Self { curr_state }
        }
    }
    
    impl Iterator for StateIterator {
        type Item = Box;
        fn next(&mut self) -> Option {
            let next_state = self
                .curr_state
                .as_ref()
                .and_then(|state| state.transition());
            std::mem::replace(&mut self.curr_state, next_state)
        }
    }
And use it:

    fn main() {
        let first_state = Box::new(FirstState { foo: 0 });
        for state in StateIterator::new(Some(first_state)) {
            println!("{:?}", state);
        }
    }
Which outputs:

    ~ state-machine git:(master)  cargo run
    First State Hit 0
    FirstState { foo: 0 }
    Second State Hit: Hello
    SecondState { bar: "Hello" }
Playground: https://play.rust-lang.org/?version=stable&mode=debug&editio...

You can work-around allocating-per-state by making a hacky enum with an array of pointers, assuming the states themselves are immutable, which gives me an idea for a library.

Re: State Machines in Rust

#98

In my experience, state machines are very nice in theory, but in practice, over time, they devolve into a mess of spaghetti code. Because they are not in a single scope, loops become the equivalent of a bunch of gotos and managing lifetimes and locks becomes a problem because you can't use scope based mechanisms such as RAII. In Rust, if you want a state machine, generators are probably the long term way to go. https…

Hand-coding state machines approximates hand-coding continuation passing style (CPS) transformation.

Generators are essentially similar to automatic CPS transformation.

Re: State Machines in Rust

#99
post #61
post #17

Earlier quoted context omitted.

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 communi…

I did not try that -- its a cool technique. Thanks for taking the time to share it.

Re: State Machines in Rust

#100
post #28

For you who enjoying using state machines but wish they did even more and/or were embedded in each other (nested state machines!), check out this thing called State Charts! Here is the initial paper from David Harel: STATECHARTS: A VISUAL FORMALISM FOR COMPLEX SYSTEMS (1987) - https://www.inf.ed.ac.uk/teaching/courses/seoc/2005_2006/res... Website with lots of info and resources: https://statecharts.github.io/ And fi…

Another formalism that's quite interesting is Petri Nets[1] (of which exist several variations, colored, hierarchical etc.).

Just, please, don't implement them in Oracle stored procedures.

[1]: https://en.wikipedia.org/wiki/Petri_net

Post reply on HN