KStateMachine / kstatemachine

KStateMachine is a Kotlin DSL library for creating state machines and statecharts.
https://kstatemachine.github.io/kstatemachine/
Boost Software License 1.0
339 stars 19 forks source link

[Question] Transition in `onFinished`? #51

Closed dimsuz closed 1 year ago

dimsuz commented 1 year ago

Is it possible to create a transition in onFinished?

My main goal is to define a sets of nested states in different gradle modules and then combine them into a "big" state machine in another gradle module which also defines a transitions between those nested children when they finish.

// this function is defined in an independent gradle module,
// it has no knowldege of "parent", containing states, so it can't mention them
// in `transition` block of `done` state
fun IState.addStates() {
  lateinit var done: State

  initialState("a") {
    transition<Event> {
      targetState = done
    }
  }

  done = finalState("done")
}

// this function is defined in main(root) gradle module, which
// depends on the above module
fun main() {
  val machine = createStateMachine {
    lateinit var s2: State

    initialState("s1") {
      addStates()
      onFinished {
       // this is currently not possible, onFinished is a simple listener
        targetState = s2
      }
    }

    s2 = state("s2") {
      // ...
    }
  }
}

IIRC, something like this is possible in scxml and xstate.js. Is this possible in kstatemachine? Am I missing some other idiom which can be used to achieve this.

nsk90 commented 1 year ago

Hi, looks that it is not possible now, maybe onFinished concept is understood differently by kstatemachine and those libraries (this should be checked)

Currently onFinished is related to whole state machine finish. When this happens machine stops receiving events. As I remember callback onFinished is defined in State instead of StateMachine because of Parallel state mode (so multiple branches may finish separately).

How do you see finishing of some State in HSM? It should trigger onFinished callback and disable all its nested transitions for further events? What if some transition from "not finished" states points to already finished state or its children?

1)

      onFinished {
         // this is currently not possible, onFinished is a simple listener
        targetState = s2
        // but this is possible, maybe it may help
        processEvent(...)
      }

2) possibly you can try implementing your case without "on finished" functionality. something like:

  initialState("s1") {
      val lastState = addStates()
      lastState {
            onEntry {
                  processEvent(...)
            }
      }
    }
dimsuz commented 1 year ago

Oh, I thought that onFinished is emitted on any compound state when one of its "final" children states is reached. In xstate.js this is called onDone and it works like this:

This is a nice way to do what I described above: define a compound state (a set of nested states) in one place and then process it's "reached final state" event in another place.

Here are the docs/examples for final/onDone from xstate.js and scxml spec (which xstate.js respects in many places).

How do you see finishing of some State in HSM? It should trigger onFinished callback and disable all its nested transitions for further events?

Yes, it seems so. When in doubt I used to consult scxml spec, there is also an extensive test suit with many examples for it somewhere on github, I'm not sure it will translate directly into kstatemachine, but can be used for reference (maybe). Or sometimes I define machine in xstate and play, it has a good visualizer here (perhaps kstatemachine will get one too sometime!)

What if some transition from "not finished" states points to already finished state or its children?

Good question, not sure here, maybe quick model of this in xstate will help to understand (I may try this later).

By the way, how will kstatemachine behave if a final state of some deeply nested state is reached? Will it stop the whole machine? This will be counterintuitive to me, because I would expect final states to be final only for the nested state.

I didn't check yet, yesterday was playing with construction of a non-trivial state machine in kstatemachine, didn't finish :)

dimsuz commented 1 year ago

I forgot to mention: the key thing in this onDone is that it's not a simple listener, but it defines a transition, i.e. I can switch to another target state in this block. Allows nice composition of nested states: when one finishes, transition to another is executed.

I can surely do something like this without final states in kstatemachine, using your suggestion above (I also had some other ideas how to adapt this for my case), but still it would be nice to have something like this if it would fit into design of kstatemachine API.

nsk90 commented 1 year ago

By the way, how will kstatemachine behave if a final state of some deeply nested state is reached? Will it stop the whole machine? This will be counterintuitive to me, because I would expect final states to be final only for the nested state.

Yes it should finish whole machine in case there is no parallel state in ancestor (parent) hierarchy. Other way only current branch (of parallels) is finished.

Good question, not sure here, maybe quick model of this in xstate will help to understand (I may try this later).

really appreciate that

onFinished looks different from onDone. I like both of this concepts, so I will see if it possible to implement. And recheck current kstatemachine behaviour))

dimsuz commented 1 year ago

Thanks!

What if some transition from "not finished" states points to already finished state or its children?

I have created a simple state machine which models this, you can play with it here by clicking on events and observing state changes.

So it works like this: after compound state reaches a final state, it will ignore all events coming to its state nodes, but after onDone transition takes state machine to another state, which is not the child of the finished state, then this compound state (catch_fish in the example) will be re-entered "from scratch".

nsk90 commented 1 year ago

Looks that finishing does not cause ignoring events, as I see from scxml spec and this sample https://stately.ai/viz/8cbd1033-0590-46fe-bc44-ec2f47ec5326

dimsuz commented 1 year ago

If we are speaking about whole state machine, then yeah, it continues to work. But when some compound state is in finished state it won't accept any external events. But parent state still can.

For example if you remove "onDone" block in the above state machine, then, when you reach catch_fish.success or catch_fish.failure, catch_fish state will ignore all events, because it's at a finished child and is considered "finished". But the parent state is still accepting events and you can send WANT_FISH, it will "restart" the catch_fish sub-state.

dimsuz commented 1 year ago

Oh! No, you are right. I have added this to catch_fish.success:

            success: {
              type: 'final',
              on: {
                'STUFF': 'wait'
              }
            },

And when in success state I can send this STUFF event, even though success is a final state!

nsk90 commented 1 year ago

Transitions on FinishedEvent will be released in v0.16.0