This is pretty cool it kind of lets you define side effects in a state machine, which is usually a feature of OOP languages but this methodology kind of defines a functional programing paradigm with side effects.
This could be a dumb question (i'm a rust and bevy newbie), but I'd be intrigued to know how/if this library could work well within bevy, for example creating a heirarchical state based character controller in a bevy game, in my experience these types of character controllers are really nice to develop with and it would be nice to explore if that was a viable option. Might not work well at all with the ecs nature of bevy however
I didnt dive that deep into bevy, but I am quite sure bevy has a similer built-in way of doing this with component structs, Maybe ask in the bevy discord for examples.
Superstates looks like inheritance. I think they have the same limitations of inheritance. It whould have been a lot better to just not get into superstate and leave the user alone. The user can always put the similar functionality into a function an call that function rather than delegating to a superstate. Multiple states can call the same function, which is equivalent to them having the same superstate. However, now, the state can call multiple function as it wants, which is similar to a state having multiple superstates, which is not possible in the library as I understood.
This library is for modelling state machines and the transitions between them, so you wouldn't use it for any sort of "cat and dog are both animals" kind of use cases. What you're basically getting here is a tree data structure with a single activated path of nodes to an edge node and a way to control transitions from that path into other paths.
@@chrisbiscardi In the end, the superstate is just for reducing code duplication, which should be done using functions. "K is the superstate of A and B" is same as "cat and dog are both animals." And I assure you, at some point you will need "K and L are superstates of A, K and M are superstates of B", which is not possible because multiple superstates are not allowed, and even if they were allowed that will lead to problems like diamond of death. Parallel to inheritance is clear here, superstates are used for reducing duplicated code, which is a bad idea. Functions should be used for reducing duplicated code. This leads to "A and B calls function K in their implementation", which can very naturally expanded to "A calls functions K or L, B calls functions K or M in their implementation."
@@chrisbiscardi Calling a function is just an implementation detail about the state logic. I will explain better with an example. Imagine there are two states we care about: working and waiting. Every second a timer event happens and occasionally a task event happens. When in working state: - on timer event: decrease the counter and switch to waiting state if it becomes 0. - on task event: crash. When in waiting state: - on timer event do nothing. - on task event: set the counter the value carried by the task event, set the led green and switch to working state. Then, the specifications change and we need a repairing state. A repair event is dispacted and we need to go the repair state and need to set the led to yellow. Now this logic must be added to both working and waiting states as given below: When in working state: - on timer event: decrease the counter and switch to waiting state if it becomes 0. - on task event: crash. - on repair event: set the led yellow and switch to repairing state. When in waiting state: - on timer event do nothing. - on task event: set the counter the value carried by the task event, set the led green and switch to working state. - on repair event: set the led yellow and switch to repairing state. But we see that as duplication of code, which is bad. Thus, we create a superstate called non-repairing state: When in non-repairing state: - on timer event: crash. - on task event: crash. - on repair event: set the led yellow and switch to repairing state. When in working state, which is a substate of non-repairing: - on timer event: decrease the counter and switch to waiting state if it becomes 0. - on task event: crash. - on repair event: delegate to superstate. When in waiting state, which is a substate of non-repairing: - on timer event: do nothing. - on task event: set the counter the value carried by the task event, set the led green and switch to working state. - on repair event: delegate to superstate. Then, specifications change again and we need an error state rather than a hard crash on error. Only the working state or repairing state can switch to error state, by the specification errors should not be happening in the waiting state. Hence, using a similar style, we create a non-error state. But we reach a wall, as the states cannot have multiple super-states. Solution is using functions for code deduplications. Here the state machine is not built for calling a function. Just that we need to implement the state logic and inside that logic we can call functions for code deduplication. We do not need the non-repairing state anymore. There are solely working, waiting, repairing and error states. Function switch_to_repairing: set the led yellow and switch to repairing state. Function switch_to_error: set the led red and switch to repairing state. When in working state: - on timer event: decrease the counter and switch to waiting state if it becomes 0. - on task event: call switch_to_error. - on repair event: call switch_to_repairing. When in waiting state: - on timer event: do nothing. - on task event: set the counter the value carried by the task event, set the led green and switch to working state. - on repair event: call switch_to_repairing. Benefits: - no need to create superstates, which are abstract and hard to understand, - no need to have infrastructure and API in the library for superstates, - delegation is explicit and no need to have super state crash on invalid events delegated to it; it does not care about event it cannot handle because it's job is not handling events, - and, the best thing, the function only knows its own purpose, which is settings up the stage for the repairing state. These criticisms are the same as what is given to inheritance. Because idea of a superstate is very similar to a supertype. Furthermore, the mechanism of delegating to a superstate is clearly the same as delegating to a superclass (for example, in Java with `super.doIt()`). Solution is clear, just do not have superstates in the library. User can call a function when there are duplications in code. Hope this clarifies what I'm trying to say.
I'm definitely interested in finding a state machine library to mirror what xstate has done in the JS ecosystem. statig is an interesting crate to try for this. Typestate might be better for UI components due the the way events can return errors that can be handled.
the typestate crate is an interesting crate. I think the major difference between statig and typestate is that typestate expects that you'll call a function for a transition and your program will handle an error if it happens, whereas statig doesn't return a value after handling events so can be used in situations where the inputs are a bit looser. typestate is a traffic light's logic and statig is user input handling. of course this is just a rough comparison and there's nothing preventing you from actually taking one crate and using it in the other way.
Can we have local storage for state as well as access to the context values? Something like this: #[state] fn on(&mut self, counter: &mut u32, event: &Event) -> Response { }
The State struct does actually contain the data for the enums for each state. . so State::On would have a counter field, and superstates would get references to that field. It's useful to either expand the macros in your editor for the examples, or to compare the macro and non-macro examples to see what code is being generated. For example, the Blinky no_macro example shows what the State would look like: github.com/mdeloof/statig/blob/063888873c901660b2391ac826b1d88096d81718/examples/no_macro/blinky/src/main.rs#L10-L13
@chris biscardi Thanks, I saw that part you linked but haven't figured it out yet. So if i assume correctly when written like this: #[state] fn message_sent(&mut self, counter: &mut u32, event: &Event) -> Response { } #[state] fn message_received(&mut self, counter: &mut u32, id: &u32, event: &Event) -> Response { } macro parses method arguments between first one "&mut self" and last one "event: &Event" or from the first one to the one before last if context is omitted. So in this case Enum generated would be: pub enum State { MessageSent { counter: isize}, MessageReceived { counter: isize, id: isize }, } If I understand it correctly? In that case thing to remember is that event argument always comes last.
@@maniacZesci you do understand that correctly with respect to the macro expansion and what State would be. The only difference is that the order of the Event doesn't matter in the arguments. The macro seems to be insensitive to the position of the arguments. The implementation of the anayze_state portion of the macro can be found here: github.com/mdeloof/statig/blob/18d5b34207f5d9e2492e52e691addd93992781a5/macro/src/analyze.rs#L280
This is pretty cool it kind of lets you define side effects in a state machine, which is usually a feature of OOP languages but this methodology kind of defines a functional programing paradigm with side effects.
13 hours ago? That's so 13 hours ago
This is 13 hours ago
1:22 For now it sounds like Rust enums. They can have state and can be nested.
This could be a dumb question (i'm a rust and bevy newbie), but I'd be intrigued to know how/if this library could work well within bevy, for example creating a heirarchical state based character controller in a bevy game, in my experience these types of character controllers are really nice to develop with and it would be nice to explore if that was a viable option. Might not work well at all with the ecs nature of bevy however
I didnt dive that deep into bevy, but I am quite sure bevy has a similer built-in way of doing this with component structs, Maybe ask in the bevy discord for examples.
This is one of the use cases I plan on exploring with this crate, so we'll find out how well it works :)
I guess you can do ResourceMut of it
Superstates looks like inheritance. I think they have the same limitations of inheritance. It whould have been a lot better to just not get into superstate and leave the user alone. The user can always put the similar functionality into a function an call that function rather than delegating to a superstate. Multiple states can call the same function, which is equivalent to them having the same superstate. However, now, the state can call multiple function as it wants, which is similar to a state having multiple superstates, which is not possible in the library as I understood.
This library is for modelling state machines and the transitions between them, so you wouldn't use it for any sort of "cat and dog are both animals" kind of use cases. What you're basically getting here is a tree data structure with a single activated path of nodes to an edge node and a way to control transitions from that path into other paths.
@@chrisbiscardi In the end, the superstate is just for reducing code duplication, which should be done using functions. "K is the superstate of A and B" is same as "cat and dog are both animals." And I assure you, at some point you will need "K and L are superstates of A, K and M are superstates of B", which is not possible because multiple superstates are not allowed, and even if they were allowed that will lead to problems like diamond of death.
Parallel to inheritance is clear here, superstates are used for reducing duplicated code, which is a bad idea. Functions should be used for reducing duplicated code. This leads to "A and B calls function K in their implementation", which can very naturally expanded to "A calls functions K or L, B calls functions K or M in their implementation."
@Cem GEÇGEL I would encourage you to research this topic a bit deeper. This tool isn't for calling functions on a shared parent.
@@chrisbiscardi Calling a function is just an implementation detail about the state logic. I will explain better with an example.
Imagine there are two states we care about: working and waiting. Every second a timer event happens and occasionally a task event happens.
When in working state:
- on timer event: decrease the counter and switch to waiting state if it becomes 0.
- on task event: crash.
When in waiting state:
- on timer event do nothing.
- on task event: set the counter the value carried by the task event, set the led green and switch to working state.
Then, the specifications change and we need a repairing state. A repair event is dispacted and we need to go the repair state and need to set the led to yellow. Now this logic must be added to both working and waiting states as given below:
When in working state:
- on timer event: decrease the counter and switch to waiting state if it becomes 0.
- on task event: crash.
- on repair event: set the led yellow and switch to repairing state.
When in waiting state:
- on timer event do nothing.
- on task event: set the counter the value carried by the task event, set the led green and switch to working state.
- on repair event: set the led yellow and switch to repairing state.
But we see that as duplication of code, which is bad. Thus, we create a superstate called non-repairing state:
When in non-repairing state:
- on timer event: crash.
- on task event: crash.
- on repair event: set the led yellow and switch to repairing state.
When in working state, which is a substate of non-repairing:
- on timer event: decrease the counter and switch to waiting state if it becomes 0.
- on task event: crash.
- on repair event: delegate to superstate.
When in waiting state, which is a substate of non-repairing:
- on timer event: do nothing.
- on task event: set the counter the value carried by the task event, set the led green and switch to working state.
- on repair event: delegate to superstate.
Then, specifications change again and we need an error state rather than a hard crash on error. Only the working state or repairing state can switch to error state, by the specification errors should not be happening in the waiting state. Hence, using a similar style, we create a non-error state. But we reach a wall, as the states cannot have multiple super-states.
Solution is using functions for code deduplications. Here the state machine is not built for calling a function. Just that we need to implement the state logic and inside that logic we can call functions for code deduplication.
We do not need the non-repairing state anymore. There are solely working, waiting, repairing and error states.
Function switch_to_repairing: set the led yellow and switch to repairing state.
Function switch_to_error: set the led red and switch to repairing state.
When in working state:
- on timer event: decrease the counter and switch to waiting state if it becomes 0.
- on task event: call switch_to_error.
- on repair event: call switch_to_repairing.
When in waiting state:
- on timer event: do nothing.
- on task event: set the counter the value carried by the task event, set the led green and switch to working state.
- on repair event: call switch_to_repairing.
Benefits:
- no need to create superstates, which are abstract and hard to understand,
- no need to have infrastructure and API in the library for superstates,
- delegation is explicit and no need to have super state crash on invalid events delegated to it; it does not care about event it cannot handle because it's job is not handling events,
- and, the best thing, the function only knows its own purpose, which is settings up the stage for the repairing state.
These criticisms are the same as what is given to inheritance. Because idea of a superstate is very similar to a supertype. Furthermore, the mechanism of delegating to a superstate is clearly the same as delegating to a superclass (for example, in Java with `super.doIt()`).
Solution is clear, just do not have superstates in the library. User can call a function when there are duplications in code.
Hope this clarifies what I'm trying to say.
Reminds me of redux a bit, I wonder if/how it could integrate with reactive libraries.
I'm definitely interested in finding a state machine library to mirror what xstate has done in the JS ecosystem.
statig is an interesting crate to try for this. Typestate might be better for UI components due the the way events can return errors that can be handled.
What’s your opinion on the typestate crate? Is it any good for implementing FSMs in Rust?
the typestate crate is an interesting crate. I think the major difference between statig and typestate is that typestate expects that you'll call a function for a transition and your program will handle an error if it happens, whereas statig doesn't return a value after handling events so can be used in situations where the inputs are a bit looser. typestate is a traffic light's logic and statig is user input handling.
of course this is just a rough comparison and there's nothing preventing you from actually taking one crate and using it in the other way.
Can we have local storage for state as well as access to the context values?
Something like this:
#[state]
fn on(&mut self, counter: &mut u32, event: &Event) -> Response { }
The State struct does actually contain the data for the enums for each state. . so State::On would have a counter field, and superstates would get references to that field.
It's useful to either expand the macros in your editor for the examples, or to compare the macro and non-macro examples to see what code is being generated. For example, the Blinky no_macro example shows what the State would look like: github.com/mdeloof/statig/blob/063888873c901660b2391ac826b1d88096d81718/examples/no_macro/blinky/src/main.rs#L10-L13
@chris biscardi Thanks, I saw that part you linked but haven't figured it out yet.
So if i assume correctly when written like this:
#[state]
fn message_sent(&mut self, counter: &mut u32, event: &Event) -> Response { }
#[state]
fn message_received(&mut self, counter: &mut u32, id: &u32, event: &Event) -> Response { }
macro parses method arguments between first one "&mut self" and last one "event: &Event" or from the first one to the one before last if context is omitted.
So in this case Enum generated would be:
pub enum State {
MessageSent { counter: isize},
MessageReceived { counter: isize, id: isize },
}
If I understand it correctly? In that case thing to remember is that event argument always comes last.
@@maniacZesci you do understand that correctly with respect to the macro expansion and what State would be.
The only difference is that the order of the Event doesn't matter in the arguments. The macro seems to be insensitive to the position of the arguments. The implementation of the anayze_state portion of the macro can be found here: github.com/mdeloof/statig/blob/18d5b34207f5d9e2492e52e691addd93992781a5/macro/src/analyze.rs#L280
You are zoomed in so close, it's hard to see context and follow along. Far beyond what's needed for readability.
I'll keep that in mind for future videos. I do have people that watch on their phones but I could go smaller than this video I think.
@@chrisbiscardi I watched on a phone. Your previous videos are fine on the phone.
I liked this text size ...I'm on 7" device!!