Hacker News new | ask | show | jobs
by cerebellum42 2267 days ago
However one big limitation of this is that this pattern cannot store the state inside another struct; it can only exist on the stack this way. So we cannot do the following:

  struct Foo<S> {
      state: State<S>,
  }
The moment we initialize Foo to take e.g. Green as its parameter, it can now no longer switch to Red in safe Rust. This is what enums are for, and unfortunately we can't use those here.

Couldn't you just declare a trait that S must implement and then declare the member in Foo as State<dyn Trait>? That trait would probably also include the next() method mentioned in the example. Of course you'd be adding dynamic dispatch here, but it should work, right?

4 comments

Yes, this is basically erasing type invariants by moving them into runtime code. Similar things have been done with things like GPIO pins in embedded code, having the pin numbers carried in the type (`struct Pin1;`) and being able to erase those for collections by moving them to runtime (`struct Pin { id: u8 }`). Unfortunately it comes with a bunch of extra implementation overhead currently, maybe in the future with const-generics it would be possible to reduce this overhead (that would end up being closer to some sort of merge between the first example and the "future directions") .
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 idiomatic Rust, but in my experience it works better and is very flexible.

The state pattern has the downside that the type system does not enforce which state changes are allowed.

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

for

    struct State<T> { 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<T: HasId> for State<T> {
        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.

I did not try that -- its a cool technique. Thanks for taking the time to share it.
That is probably the way to go, agreed.
I think that the whole point is to do this statically.
Of course dynamic dispatch helps, but using it for a state machine is going to raise eyebrows and kill performance.
I think this would depend on the application. Its possibly a problem in a parser of large data, but in a network protocol or business rule its not going make any difference at all.
If you are using a compiled, systems programming language like Rust, you do care about performance to begin with.

Specially in things like parsing or a network protocol!

I think what they're saying is that it literally makes no difference, not negligible difference. At some point you have to do the branching that decides which part of the logic in the state machine runs next. Dynamic dispatch is essentially just a way to do this kind of branching.