Statechart Data 2 - Events and Entry

Second in a series of posts about how state machines can store and manipulate data. Figuring out how state machines integrate cleanly into a codebase -- how they do this essential task of data management - is, i think, one of their biggest impediments to adoption.

Jumping right in, one of the most obvious spots for data storage ( other than global storage ) are inside the events that the state machine is responding to.  There are good things, and bad things, about using events for data handling, and it turns out not all statechart implementations are created equal...

Event Transmission:

The purpose of every statechart is to listen for incoming events, and take some sort of action as a result. Events often have data associated with them. A mouse click event, for instance, might have information about which button was pressed, where the mouse was on the screen, and whether it was over a piece of ui at the time. ( Even delegates have data, in the form of the function parameters passed to the delegate function. )

A form of data storage, therefore, can be derived by bouncing data between a state machine and the systems that a state machine interacts with. Here's the example of the main menu again:

   Main Menu:
   - entry / display_menu();
   - button[0] >> New Game.
   - button[1] >> Continue Game.
   - button[2] >> Options.
   ... 
   Continue Game:
   - init >> Load Saves.
      Load Saves:
      - entry /load();
      - loaded >> Select Game.
      Select Game:
      - entry /display_saves();
      - selected >> Confirm Choice.
      Confirm Choice:
      - entry /prompt_user_with_dialog();
      - response[no]  >> Select Game.
      - response[yes] >> Run Game.
    ...
 
Continue Game's initial state is Load Saves, and that initial state's entry action calls load(). That call implies the creation of some activity which actually goes out and does the enumeration of the saved games.  Load Saves then sits and waits to hear the "loaded" event.

   class SavedGamesLoaded:Event {
      list<savedgame> saves;
   };

   class SaveGameLoader {
       void SaveGameLoader(string save_game_path, event_queue);
       void start_load();
       void finished() {
         event_queue->queue_event( new SavedGamesLoadedEvent(this->saves); ); 
       };
   private:
       list<savedgame> saves;
   };

   // all this is just pseudo code; 
   // but, you get the idea...

   void LoadSaves::load() {
      (new SaveGameLoader("some/file/path", event_queue ))->start_load();
   }
   void LoadSaves::loaded( SavedGamesLoaded evt ) { 
      // ....
   }

The question then becomes, what to do with the data. Notice how in the statechart above, it's the "Select Game" state that displays the list of things to save. If the first state has no way to pass data off to the next state, then it would be "Load Saves", not "Select Game" that has to display the list of save games. To put that another way, how does LoadSaves::loaded() tell SelectGame about the evt.saves data?

Similarly, "Select Game" listens for the user's selection, but it's "Confirm Choice" needs that choice in order to, first, prompt the user to confirm that choice and, then, to pass that choice off to "Run Game".

It *is* possible to re-arrange our states to manage this hand off. The same chart annotated with the necessary event data, and rearranged to bounce the data around would look something like this:

   Continue Game:
   - init >> Load Saves.
      Load Saves:
      - entry/ load();
      - loaded(saves) / display_saves(saves) >> Select Game. 
      Select Game:
      - selected(saves+choice) / prompt(saves+choice) >>  Confirm Choice.
      Confirm Choice:
      - response(saves+choice) [no]  / display_saves(saves) >> Select Game.
      - response(saves+choice) [yes] / start_game(choice) >> Run Game. 

First, notice how the state which initiated the actions had to change. For instance, since the loaded event handler gets the saves data through the event, and since display_saves() was the action that needed that data: display_saves() got shifted from "Select Game" to "Load Saves" so that the event could pass it to the action. 

Second, notice how even though "Confirm Choice" didn't need the whole list of "saves" before, it needs that list now so that it can re-display the choices before entering "Select Game", essentially passing the data back to "Select Game".

On the list of positives is that we've managed to bounce the list of saved games from place to place, but we've never had to record those games anywhere permanent. Unlike global data, it's not around forever; "Run Game" and everything after never even needs to know about it.

On the list of negatives, however: this arrangement has completely broken the encapsulation of each state. In the original chart: "Select Game" knew how to to display the list of saved games. Now, it does not. If you forget to call "display_saves()” before entering "Select Game" all hell will break loose.

In general, outfitting events with data and consuming that data in state actions is good -- there's no better way to get the list of saved games from whatever activity is actually loading the saves and into the statemachine than by using an event to transmit that data -- but it’s not a complete solution when you're crossing state boundaries.

Leveraging Entry:

It's notable that the rearranged chart ignores "entry". If we could leverage entry, and use it to pass the preconditions each state needs to do its thing, life would be better.

   Continue Game:
   - init >> Load Saves.
      Load Saves:
      - entry/ load();
      - loaded(saves) >> Select Game(saves). 
      Select Game:
      - entry(saves) / display_saves(saves) 
      - selected(saves+choice) >>  Confirm Choice(saves+choice).
      Confirm Choice:
      - entry(saves+choices) / prompt(saves+choice) 
      - response(saves+choice) [no]  / >> Select Game(saves).
      - response(saves+choice) [yes] / start_game(choice) >> Run Game(choice). 

We’re still passing around “saves” to "Confirm Choice" even though it doesn't need the whole list of save games, just so that response[no] can pass that data back to "Select Game" ( if a user wants to change their choice ), but it’s not bad. By leveraging entry, we have maintained state encapsulation.

Notice that in each case, the data flowing to a state's entry is the same as (or a subset of) the event data passed with the event that caused to the state transition to fire. ( eg. the "loaded" event passes the "saves" data, and that's exactly the data "Select Game" needs. ) This proves to be a general condition of well defined states and events. Meaning: to "customize" entry, all we really need access to is the event that caused the transition.

So, can we do this? Can we get access to that event? Interestingly enough, it depends on the statechart implementation you are using. Boost MSM and CHSMC both support customizing entry, while Boost.Statechart, Samek’s QEP, and the SMC all lack such support. ( SMC even specifically mentions this as a problem. )

Is it time to flog the developers, then? Is one group of implementation "right" and the other 'wrong." After digging around on this question i found that the UML 1.1 spec says:
3.77.1 The trigger for a transition is the..... event labeling the transition. The event may have parameters, which are accessible by the actions specified on the transition as well as in the corresponding exit and entry actions associated with the source and target states respectively...
But, upon further investigation -- version 1.1 was written 12 years ago(!) -- it seems that the above section was dropped from subsequent versions of the spec. It may have been overlooked, or deemed just an implementation detail -- i haven't found anything that says definitively one way or the other. 

If it's never been said before: getting access to the transition event in entry is extremely valuable. It would be a boon to statechart development if this was still in the spec.  For systems that don't support this directly, it's so worthwhile, you might even consider modifying the system to do so. ( Perhaps, for instance, a "last event" variable stored as a member of the state machine object. )

But, even though it *is* valuable, we *are* still bouncing around the "saves" data quite a bit. While that works -- it's not necessarily pretty. From a re-usability standpoint, it would be nice if "Confirm Choices" didn't have to know about saves. There are still two ( and a half ) more options left. I'll look at them in the next posts.

0 comments: