1

I have multiple state machines (7) managed by a "main" function. Because of system constraint this will be run on an old qtweb browser with ES5/IE11-like behaviour. So no promises etc. I'm already using Babel to avoid some issues like let/const not accepted etc. My only "reactive" option at the moment is callbacks.

At the moment the codebase is growing and the state transitions are being called from various point of the main function or its sub functions and it's getting hard to keep track of them.

Code example of an interaction with multiple fsm

  • main is the main loop
  • transitionFSMX is the function moving the fsm X from a state to another
  • fsmStateX is the current state of the fsm number X
// a generic global variable
let counter = 18;

// my main function with the logic
function main() {
...
  switch(fsmState1) {
    case state1: {fsm1Check(); break;}
    ...
  }
...
}

// code to be run if the fsm is in state 1
function fsm1Check() {
  if (x==1) {
   // transitioning another fsm
   transitionFSM2("xEvent")
  }
  // incrementing a generic global variable
  counter++;
}

Or more generic checks

let y = 4

function main() {
  // generic check
  if (y==3) { 
   transitionFSM2("yEvent")
   transitionFSM1("yEvent")
    y+=2;
  } else {
     transitionFSM3("yEvent")
     y+=1;
  }
}

The most simple solution I came up with yesterday when I noticed I started having issues managing all of this is having a variable for each fsm with all the events that change the fsm, like this:

var fsm1Events = {

  onXValueChange(): function() {
     transitionFSM1("xEvent")
  },

  onYValueChange(change): function(change) {
     fsm1StateYVariable = change;
     transitionFSM1("yEvent")
  }
}

Is there a better pattern?

EDIT: I remembered I could also implement with vanilla js the pub/sub and subscriber patterns but I´d still like to get more opinions

4
  • I'm somewhat confused by your code - and I don't mean this as a way to bash you - it's just that this might be an indication that you yourself aren't clear with exactly what finite state machines you have, and what are their states, and who's responsible for each transition. E.g., if you left this project alone for a couple a weeks, and came back to it, would you be able to quickly figure out what's even going on? So, it would be helpful to you and to us if you could sort that out on a conceptual level. E.g., would you be able to draw a diagram? 1/2 Oct 15, 2021 at 10:55
  • Also, could you provide more context? For example, your fsm1Check seems to directly invoke a transition on a different FSM, instead of invoking FSM2's client-facing interface? Or is the client-facing interface represented by the events ("xEvent", "yEvent")? Or is FSM2 actually a different state in the same FSM? You showed us bits and pieces, but it's hard to complete the puzzle. Are you using other global variables (x, y) as event parameters? What is the counter for and why it's the responsibility of the FSMs to increment it? Do they use it at all? 2/2 Oct 15, 2021 at 10:55
  • My bad, I was trying to use as little code as possible but it seems I've shown too litlle. > you yourself aren't clear with exactly what finite state machines you have, and what are their states, and who's responsible for each transition. I have 7 fsm and a "main" function which orchestrate everything. This function starts all the transitions of the fsm.
    – The Mark
    Oct 15, 2021 at 12:52
  • > For example, your fsm1Check seems to directly invoke a transition on a different FSM, instead of invoking FSM2's client-facing interface? fsm1Check is a check from the main function on a value of the first fsm. If it's a certain value, I'll have a transition for the fsm2, otherwise I ll just increment a generic global variable. I'll add some more context now
    – The Mark
    Oct 15, 2021 at 12:54

1 Answer 1

3

One way to look at this is that a finite state machine is basically just a function + a state variable, that, when given an input, updates the state variable, and produces an (input & state)-dependent output. And maybe has some side-effects on state transitions.

This means that, instead of having a bunch of tangled if-statements, you can have your FSMs interact by feeding the output of one FSM into another. Even if you have a FSM that currently has no outputs and simply invokes a transition on a different FSM, change it so that it returns an output representing its current state, and let the other FSM make a decision using that as input. Sometimes, if two or more states have the same effect on some other FSM, and no other FSM cares about the difference, you can encode those states as the same output value. Note that you'll likely have to invent outputs other than "xEvent" and "yEvent" for the later part of the flow. Think about what the FSM actually computes (what it actually does), and design your outputs to reflect that (and serve as meaningful inputs later on).

What this does is, it frees main() from having to examine each event and state machine in order to decide which method to call. Instead, it can focus on orchestrating the flow of inputs and outputs, and let the FSM themselves decide how to handle events internally.

Keep the core logic of each FSM in one place, and keep it self contained. E.g.

// this is basically a constructor
function createFsm(/* parameters, if any */) {
  var state = "initial_state";

  function transitionFromInitialState(input) { 
      // switch on input and update 'state' var
      // invoke any side effects (DO NOT transition other FSMs here)
      //     e.g., you can do 'counter++' here
      // return output
  }

  function transitionFromState1(input) { 
      // switch on input and update 'state' var
      // invoke any side effects (DO NOT transition other FSMs here)
      // return output
  }

  function transitionFromState2(input) { 
      // switch on input and update 'state' var
      // invoke any side effects (DO NOT transition other FSMs here)
      // return output

  }

  // ...

  return {
    transition: function (input) {
      switch(state) {
        case state_initial: return transitionFromInitialState(input);
        case state_1: return transitionFromState1(input);
        case state_2: return transitionFromState2(input);
        ...
      }
    }
  };
}

This is not that dissimilar from the solution you came up with - except that I pulled all the related code together, and introduced outputs.

If all your state machines share the same logic, but start out in different states, consume different (sub)sets of inputs, or something along those lines, then you need just this single creator function - that perhaps takes some parameters to choose an initial state. You then just create 7 instances.

// in main

let fsm1 = createFsm("initial_state1");
let fsm2 = createFsm("initial_state2");
let fsm3 = createFsm("initial_state3");
...

If your state machines have different logic, then provide a creator function for each distinct behavior.

// in main

let fsm1 = createFsmA();

let fsm2 = createFsmB();
let fsm3 = createFsmB();

let fsm4 = createFsmC();
...

Write the orchestration logic:


// Maybe this is called in a loop, or invoked as a chain of
// computations every time a specific initiating event happens
// (meaning, some of the final outputs may be returned, and can 
// become part of the input during the next iteration/event)

function compute(initialInput) {

  var value = initialInput;

  // Direct output ---to--> input chain
  value = fsm1.transition(value);
  value = fsm2.transition(value);
  value = fsm3.transition(value);

  // Same output as input to two FSMs
  var valueFsm4 = fsm4.transition(value);
  var valueFsm5 = fsm5.transition(value);

  // Result of two FSMs as input to the next one
  value = fsm5.transition(valueFsm4, valueFsm5);

  // etc... Think of it as of a flowchart

  return /* whatever return values are relevant */
}

Finally, in main:


// maybe there's a loop
var lastComputationResult = null;
while (running()) {
  var initialInput = getInitialFsmInput(lastComputationResult);
  lastComputationResult = compute(initialInput);
}

------------------------------------------------------------

// maybe it's done in an event handler
var lastComputationResult = null;
addEventHandler(function(event) {
  var initialInput = getInitialFsmInput(event, lastComputationResult);
  lastComputationResult = compute(initialInput);
});

------------------------------------------------------------

// or some other variation
// ...

Adjust the transition functions (and inputs and outputs) as necessary for what you're doing. E.g., an "input" can be a combination of values (so your transition functions can take either a composite object, or several parameters) - but keep things manageable (you don't want a combinatorial explosion). Parts of the inputs or outputs could also be unrelated to the transition logic itself, but can just serve as the parameters to the side-effects.

1
  • Thanks for taking the time to write such an in-depth answer, I'll read and try it tomorrow.
    – The Mark
    Oct 15, 2021 at 22:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.