Where communities thrive


  • Join over 1.5M+ people
  • Join over 100K+ communities
  • Free without limits
  • Create your own community
People
Activity
    john-j-mclaughlin
    @john-j-mclaughlin
    What's the difference between
    1. executing some code associated with the event (assuming the FSM is in a valid state to process that event) and transitioning to a new state
      and
    2. transitioning to a new state and executing some code associated with the event?
      I see no distinction.
    john-j-mclaughlin
    @john-j-mclaughlin
    Unless you're assuming the event code is dependent upon the new state's "on entry" code having already run. In which case this sounds like a brittle approach.
    Eric Walkingshaw
    @walkie

    Some differences between executing code after a transition vs. the enter handler for the new state:

    1. There may be several different ways to transition into the new state and we don't want to execute the code for all of them. This could be worked around by passing a flag as a transition argument, but this complicates the code in other ways since now that argument must be passed whenever we transition to that state.

    2. Code executed after the transition can use variables in scope in the current handler. This can also be worked around by passing transition arguments, but now things get really messy if we have multiple ways to enter the new state.

    I'm not sure how important either of these scenarios are in real systems, and I like the appeal of the simpler model. However, I also think there's an argument for supporting mid-handler transitions for expressiveness reasons, even if we don't have concrete applications in mind.

    My general philosophy in language design is to err on the side of expressiveness unless there are specific correctness/safety criteria we have in mind that would be violated. In this case, the criterion could be "the last code associated with a state that is executed is the exit handler". If that is a criterion we want, then we should disallow mid-handler transitions. If that is not a criterion that we care about (and there isn't some other one that would be violated), then we should allow it by default.

    Eric Walkingshaw
    @walkie
    Always helpful to write down these kinds of criteria as we discover them too since it helps to make more consistent/informed design decisions in the future.
    Mark Truluck
    @frame-lang
    Great discussion and I gotta say I'm really excited to start to get into this level of thinking about how to state machine systems ought to be designed and used . One of the goals I had in creating Frame was to make these kinds of questions easier to see and discuss and not be buried in the details of the target languages.
    I'll add some more thoughts later
    john-j-mclaughlin
    @john-j-mclaughlin

    To address the type of behavior you describe, I have used 1 of 2 approaches in the past. The first is with substates of the target state, where the substate reflects how it got there. The substate can have "how" related code to execute, and the common parent would have the general state code.

    The second approach is supported in my implementation... All handler code (state_entry, state_exit, on_event, on_timer, etc.) has access to the triggering event so this code can have conditional logic based on the event.

    I generally find the first approach preferable since it's exposed in the FSM structure and is therefore documented.

    Eric Walkingshaw
    @walkie
    Thanks, John. I'm a lot less familiar with using state machines in practice, so good to know there are patterns for recovering this behavior in a sane way if mid-handler transitions are removed.
    john-j-mclaughlin
    @john-j-mclaughlin
    Eric, I re-read what you were asking about and I may have read more complexity into the question that was actually present. If you were asking how to get code associated with the event, and not with the target state, it's even more simple. In my grammar it would be expressed something like this:
    state_machine(fsm) {
    
        state(A) {
            on_event("Event_1", ../B) ${
                // code to execute when Event_1 occurs in state A
                // followed by transition to state B
            $}
            on_event("Event_2", ../B) ${
                // code to execute when Event_2 occurs in state A
                // followed by transition to state B
            $}
            on_entry ${
                // code to execute when entering state A
            $}
        }
    
        state(B) {
            on_entry ${
                // code to execute when entering state B
            $}
        }
    }
    Eric Walkingshaw
    @walkie

    I think you read the proper amount of complexity the first time! :-)

    The problem is if you have code you want to execute on Event_1 (and not on Event_2) after the transition to state B. Currently, Frame allows you to specify a transition in the middle of an event handler, so you can (1) do some stuff, (2) transition to B, (3) do some more stuff in the same context that you did (1). You can't put the stuff from (3) in the on_entry handler for B because it shouldn't be executed if we reach B via Event_2.

    Your solution with sub-states solves this, I think, because then you can put the stuff from (3) in the on_entry handler for the corresponding sub-state. However, there is still the problem of what if the stuff from (3) depends on variables in scope in the A.Event_1 handler? You can pass all this information along, but now things are getting more complicated.

    I don't have a concrete use case for this in mind and I'm not opposed to deciding for other reasons that they should be disallowed. Just trying to highlight that mid-handler transitions do provide some expressiveness that is lost by forbidding them. Note here that I don't mean "expressiveness" in the absolute sense (I'm sure both approaches can ultimately do the same thing) but rather in the practical sense (you have to jump through more hoops to do it without this feature, e.g. use sub-states and propagate the needed variables along via arguments).

    john-j-mclaughlin
    @john-j-mclaughlin
    Well, I guess the 2nd approach would look like this:
    state_machine(fsm) {
    
        state(A) {
            on_event("Event_1", ../B);
            on_event("Event_2", ../B);
            on_entry ${
                // code to execute when entering state A
            $}
        }
    
        state(B) {
            on_entry ${
                // code to execute when entering state B
                switch sourceEvent.id {
                case "Event_1":
                    // code to execute when entering state B from Event_1
                case "Event_2":
                    // code to execute when entering state B from Event_2
                }
                // code to execute when entering state B
            $}
        }
    }
    Eric Walkingshaw
    @walkie
    Yeah, that looks nice (although the scope is still different!) but Frame doesn't have a way to access the source event, as far as I know, so you'd have to pass in a parameter to match on.
    john-j-mclaughlin
    @john-j-mclaughlin
    Not sure what "scope" you refer to. This is not like a call stack frame. The "variables" which need to be persisted for later access are just member attributes of the state machine root object, or of one of the substate objects.
    The data for an event (if any is required) are just properties of the event object and are accessible by any handers invoked by the event.
    Eric Walkingshaw
    @walkie
    That's not the way frame works though, which is the language we're making this decision about. :-) In Frame, there are potentially state variables, state parameters, and event parameters, which would all be in scope in a handler in state A, and would not be in scope in the on_entry handler in state B, and there would be no way to access them any more from the handler in state B.
    john-j-mclaughlin
    @john-j-mclaughlin
    Ah... OK... I cannot speak specifically to Frame :)
    Mark Truluck
    @frame-lang
    Just wrapped up the day and finally have a moment to comment but looks like a great discussion. I can argue it either way theoretically but there may be a forcing function that dictates a particular decision. In v6.0 I want to make the transition type to be configurable. Currently all transitions are what I'm thinking to call "immediate". As we have been discussing, I would like to get "deferred" transitions in next. If it was confusing to have actions happen after an immediate transition, it would be nonsensical to have them after a deferred transition. In effect, for deferred, one would always execute all actions prior to the transition regardless of if you specified them before or after the transition because, well, the the transitions were deferred :).
    So as to not have completely different behavior between the two architectures, I am leaning towards having the grammar enforce a return immediately after a transition. What do ya'll think?
    The deferred architecture example: https://dotnetfiddle.net/rQaB9R
    Mark Truluck
    @frame-lang
    On a different point that was raised - the frame event is referenceable using the @ token. Search for Frame Event here: https://frame-lang.org/notation/
    You can also reference it's component parts as well and pass the event as well as its parts to actions
    action1(@ @|| @["p1"] @^) // etc
    So I believe you can pass the "source event" to an interface call like this (but haven't tried it yet :) )
    iface1(@)
    Mark Truluck
    @frame-lang
    I'll give it a try later.
    Eric Walkingshaw
    @walkie
    Hi Mark, that makes sense to me. I'll adjust my (still in progress...) refactor of the Rust backend to only support transitions at the end of the handler. Currently, since the frontend doesn't rule these programs out, the backend will just generate code that doesn't compile (due to an ownership issue). But once the frontend is modified to enforce returns after transitions, hopefully the errors will be a bit more comprehensible.
    Mark Truluck
    @frame-lang
    Considering the use of https://readthedocs.org/ for documentation. I agree getting the grammar documented is a top priority. Any other ideas for the documentation?
    I found that resource when learning Godot: https://docs.godotengine.org/en/stable/index.html
    Eric Walkingshaw
    @walkie

    Using readthedocs.org seems like a solid choice to me.

    The documentation that you have already on frame-lang.org is a great start I think. Just needs to be expanded a bit and a way to make it easier to navigate. Using readthedocs.org should be good for that because you'll get section header links in the menu and so on, which will help a lot.

    The demos you have linked at the bottom of this page were also very helpful to us getting started. Might be worthwhile to have one medium complexity example like this that is explained piece-by-piece in the documentation, as a sort of tutorial. Then you can just include links to other examples like you have already.

    Bradley Nelson
    @bradnelson
    I've liked readthedocs as well :thumbsup: (Also came across it when learning Godot!)