Let's try to talk about hierarchical finite automata in general and their support in SObjectizer-5 in particular

    Finite automata are probably one of the most fundamental and widely used concepts in programming. State machines (SCs) are actively used in a variety of application niches. In particular, in such niches as the automated process control system and telecom, which have been dealt with, spacecraft are found a little less frequently than at every step.

    Therefore, in this article we will try to talk about spacecraft, primarily about hierarchical finite automata and their advanced capabilities. And to tell a little about the support of spacecraft in SObjectizer-5 , the “actor” framework for C ++. One of those two few that are open, free, cross-platform, and still alive.

    Even if you are not interested in SObjectizer, but, say, you have never heard of hierarchical finite automata or how advanced spacecraft such as state input or output handlers for states or state history are useful, then it may be interesting to look under the cat and read at least the first part of the article.

    General words about state machines


    We will not try to carry out in the article a complete educational program on the topic of automata and such types of them as finite automata . The reader must have at least a basic understanding of these types of entities.

    Advanced state machines and their capabilities


    The spacecraft has several “advanced” capabilities that greatly enhance the usability of the spacecraft in the program. Let's take a quick look at these “advanced” features.

    Disclaimer: if the reader is well acquainted with state diagrams from UML, then he will not find anything new for himself here.

    Hierarchical finite automata


    Perhaps the most important and valuable opportunity is the organization of hierarchy / nesting of states. Since it is the ability to put states into each other eliminates the "explosion" of the number of transitions from state to state as the complexity of the spacecraft increases.

    It is harder to explain this with words than with an example. Therefore, let us imagine that we have an infokiosk, on the screen of which a welcome message is first displayed. The user can select the “Services” item and go to the section for selecting the services he needs. Or you can select the “Personal Account” item and go to the section of work with your personal data and services. Or you can select the Help section. So far everything seems to be simple and can be represented by the following state diagram (as simplified as possible):



    But let's try to make it so that when you click on the Cancel button, the user can return from any section to the start page with a welcome message:



    The scheme is complicated, but still under control. However, let us recall that in the “Services” section we may have several more subsections, for example, “Popular Services”, “New Services” and “Full List”. And from each of these sections you also need to return to the start page. Our simple spacecraft is becoming more and more complex:



    But this is still not all. After all, we have not yet taken into account the “Back” button, by which we need to return to the previous section. Let's add another reaction to the “Back” button and see what we get:



    Yes, now we see the way to real fun. But we have not even considered the subsections in the sections "Personal Account" and "Help" ... If we start, then almost immediately our simple, at first, spacecraft will turn into something unimaginable.

    This is where nesting of states comes to the rescue. Let's imagine that we have only two top-level states: WelcomeScreen and UserSelection. All our sections (i.e., “Services”, “My Account” and “Help”) will be “nested” in the UserSelection state. We can say that the ServicesScreen, ProfileScreen, and HelpScreen states will be children of a UserSelection. And since they are children, they will inherit the reaction to some signals from their parental state. Therefore, the reaction to the "Cancel" button, we can define in UserSelection. But we have no need to define this reaction in all subsidiary substates. What makes our spacecraft more concise and understandable:



    Here you can note that the reaction for "Cancel" and "Back" we defined in UserSelection. And this reaction to the Cancel button works for all UserSelection substates without exception (including another composite substate ServicesSelection). But in the ServicesSelection sub-state, the reaction to the “Back” button has its own — the return is not in WelcomScreen, but in ServicesScreen.

    ACs that use state hierarchy / nesting are called hierarchical finite automata (ICA).

    Reaction to entry / exit to / from state


    A very useful feature is the ability to assign a response to the entrance to a particular state, as well as the response to the exit from the state. So, in the example above with the infokiosk, you can hang a handler at the entrance to each of the states that will change the contents of the infokiosk screen.

    The previous example can be slightly extended. Suppose we have two substates in WelcomScreen: BrightWelcomScreen, in which the screen will be highlighted normally, and DarkWelcomScreen, in which the screen brightness will be reduced. We can make an entry handler in DarkWelcomScreen, which will reduce the screen brightness. And exit processor from DarkWelcomScreen, which will restore normal brightness.



    Automatic change of state after a specified time


    At times, it may be necessary to limit the duration of a spacecraft in a particular state. So, in the example above, we can limit the time our ICA stays in the BrightWelcomScreen state to one minute. As soon as the minute expires, the ICA is automatically transferred to the DarkWelcomScreen state.

    History of the state of spacecraft


    Another very useful feature of ICA is the history of the state of spacecraft.

    Let's imagine that we have some kind of abstract IKA like this:



    This our IKA can go from TopLevelState1 to TopLevelState2 and back. But inside TopLevelState1 there are several nested states. If IKA just goes from TopLevelState2 to TopLevelState1, then two states are activated at once: TopLevelState1 and NestedState1. NestedState1 is activated because it is the initial substate of the TopLevelState1 state.

    Now imagine that our ICA further changed its state from NestedState1 to NestedState2. Inside NestedState2, the substate InternalState1 was activated (because it was the initial state for NestedState2). And from InternalState1 we went to InternalState2. Thus, the following states are simultaneously active: TopLevelState1, NestedState2 and InternalState2. And here we go to TopLevelState2 (i.e., we generally left TopLevelState1).

    TopLevelState2 becomes active. Then we want to return to TopLevelState1. It is in TopLevelState1, and not to any particular substate in TopLevelState1.

    So, from TopLevelState2, we go to TopLevelState1 and where do we go?

    If TopLevelState1 does not have a history, then we will come to TopLevelState1 and NestedState1 (since NestedState1 is the initial substate for TopLevelState1). Those. the whole story about the transitions inside TopLevelState1, which were carried out before going to TopLevelState2, was completely lost.

    If TopLevelState1 has a so-called. shallow history, then when returning from TopLevelState2 to TopLevelState1, we end up in NestedState2 and InternalState1. In NestedState2 we fall because it is recorded in the history of the state TopLevelState1. And we get to InternalState1 because it is the initial one for NestedState2. It turns out that in the surface history for TopLevelState1 information is stored only on the substates of the very first level. The history of nested states in these substates is not preserved.

    But if TopLevelState1 has a deep history (deep history), then when you return from TopLevelState2 to TopLevelState1, we end up in NestedState2 and InternalState2. Because in the deep history, complete information about the active substates is stored, regardless of their depth.

    Orthogonal states


    So far, we have considered ICA in which only one of the substates could be active within the state. But at times there may be situations when there should be several simultaneously active substates in a specific ICA state. Such substates are called orthogonal states.

    A classic example that demonstrates orthogonal states is the computer keyboard we are used to and its NumLock, CapsLock and ScrollLock modes. We can say that working with NumLock / CapsLock / ScrollLock is described by orthogonal substates within the Active state:



    Everything you wanted to know about state machines, but ...


    In general, there is a fundamental article on the formal notation for state diagrams from David Harel: Statecharts: A Visual Formalism For Complex Systems (1987) .

    They understand various situations that can occur when working with finite automata on the example of control of a conventional electronic clock. If someone did not read it, I highly recommend it. In principle, everything that Harel described was then transferred to the UML notation. But when you read the description of state diagrams from UML, I don’t always understand what, for what and when. But in the article by Harel, the presentation goes from simple situations to more complex ones. And you are better aware of all the power that finite automata hide in you.

    State machines in SObjectizer


    Then we will talk about SObjectizer and its specificity. If the examples below are not completely clear to you, then it may make sense to learn more about SObjectizer. For example, from our review article about SObjecizer and several subsequent ones, which introduce readers to SObjectizer, moving from simple to complex ( first article, second and third ).

    Agents in SObjectizer are state machines


    From the very beginning, the agents in SObjectizer were finite automata with pronounced states. Even if the agent developer did not describe any eigenstate in his agent class, the agent still had a default state, which was used by default. For example, if the developer has made such a trivial agent:
    classsimple_demofinal :public so_5::agent_t {
    public:
      // Сигнал для того, чтобы агент напечатал на консоль свой статус.structhow_are_youfinal :public so_5::signal_t {};
      // Сигнал для того, чтобы агент завершил свою работу.structquitfinal :public so_5::signal_t {};
      // Т.к. агент очень простой, то делаем все подписки в конструкторе.
      simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
        so_subscribe_self()
          .event<how_are_you>([]{ std::cout << "I'm fine!" << std::endl; })
          .event<quit>([this]{ so_deregister_agent_coop_normally(); });
      }
    };

    then he may not even suspect that in reality all the subscriptions made by him are made for the default state. But if the developer adds his own states to the agent, then you have to think about correctly signing the agent in the correct state. Here, for example, a simple (and, as usual) incorrect modification of the agent shown above:
    classsimple_demofinal :public so_5::agent_t {
      // Состояние, которое означает, что агент свободен.state_t st_free{this};
      // Состояние, которое означает, что агент занят.state_t st_busy{this};
    public:
      // Сигнал для того, чтобы агент напечатал на консоль свой статус.structhow_are_youfinal :public so_5::signal_t {};
      // Сигнал для того, чтобы агент завершил свою работу.structquitfinal :public so_5::signal_t {};
      // Т.к. агент очень простой, то делаем все подписки в конструкторе.
      simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
        so_subscribe_self()
          .event<quit>([this]{ so_deregister_agent_coop_normally(); });
        // На сообщение how_are_you реагируем по разному, в зависимости от состояния.
        st_free.event([]{ std::cout << "I'm free" << std::endl; });
        st_busy.event([]{ std::cout << "I'm busy" << std::endl; });
        // Начинаем работать в состоянии st_free.this >>= st_free; 
      }
    };

    We defined two different handlers for the how_are_you signal, each for its own state.

    And the mistake in this modification of the agent simple_demo is that while in st_free or st_busy the agent will not respond to quit at all, since we left the quit subscription in the default state, but did not make the appropriate subscriptions for st_free and st_busy. A simple and obvious way to fix this problem is to add the appropriate subscriptions to st_free and st_busy:
      simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
        // На сообщение how_are_you реагируем по разному, в зависимости от состояния.
        st_free
          .event([]{ std::cout << "I'm free" << std::endl; })
          .event<quit>([this]{ so_deregister_agent_coop_normally(); });
        st_busy
          .event([]{ std::cout << "I'm busy" << std::endl; })
          .event<quit>([this]{ so_deregister_agent_coop_normally(); });
        // Начинаем работать в состоянии st_free.this >>= st_free; 
      }

    True, this method smacks of copy-paste, which is not good. You can get rid of copy-paste by entering the common parental state for st_free and st_busy:
    classsimple_demofinal :public so_5::agent_t {
      // Общее родительское состояние для всех подсостояний.state_t st_basic{this};
      // Состояние, которое означает, что агент свободен.// Является также начальным подсостоянием для st_basic.state_t st_free{initial_substate_of{st_basic}};
      // Состояние, которое означает, что агент занят.// Является обычным подсостоянием для st_basic.state_t st_busy{substate_of{st_basic}};
    public:
      // Сигнал для того, чтобы агент напечатал на консоль свой статус.structhow_are_youfinal :public so_5::signal_t {};
      // Сигнал для того, чтобы агент завершил свою работу.structquitfinal :public so_5::signal_t {};
      // Т.к. агент очень простой, то делаем все подписки в конструкторе.
      simple_demo(context_t ctx) : so_5::agent_t{std::move(ctx)} {
        // Обработчик для quit определяем в st_basic и этот обработчик// будет "унаследован" вложенными подсостояниями.
        st_basic.event<quit>([this]{ so_deregister_agent_coop_normally(); });
        // На сообщение how_are_you реагируем по разному, в зависимости от состояния.
        st_free.event([]{ std::cout << "I'm free" << std::endl; });
        st_busy.event([]{ std::cout << "I'm busy" << std::endl; });
        // Начинаем работать в состоянии st_free.this >>= st_free; 
      }
    };

    For the sake of justice, it is necessary to add that, initially, in SObjectizer, agents could only be simple finite automata. Support for hierarchical spacecraft appeared relatively recently, in January 2016.

    Why in SObjectizer agents are finite automata?


    This question has a very simple answer: this is how SObjectizer’s roots grow from the world of automated process control systems, and there finite automata are used very often. Therefore, we considered it necessary that the agents in SObjectizer also be finite automata. This is very convenient if in the application for which the SObjectizer is attempted to apply, QA are used. And the default state, which all agents have, makes it possible not to think about spacecraft, if the use of spacecraft is not required.

    In principle, if you look at the Model Actors itself, and on the principles on which this model is built:

    • an actor is an entity with behavior;
    • actors respond to incoming messages;
    • having received the message the actor can:
      • send a certain number of messages to other actors;
      • create a number of new actors;
      • define for yourself a new behavior for handling subsequent messages.

    One can find a strong similarity between simple spacecraft and actors. It can even be said that the actors are simple finite automata.

    What features does the advanced SObjectizer state machine support?


    Of the above capabilities of the advanced finite automata, SObjectizer supports all, with the exception of orthogonal states. The rest of the goodies, like nested states, input / output handlers, restrictions on the time spent in the state, history for the states, are supported.

    With the support of orthogonal states from the first time did not grow together. On the one hand, the internal architecture of SObjectizer was not designed to support several independent and simultaneously active states for the agent. On the other hand, there are ideological questions about how an agent with orthogonal states should behave. The tangle of these questions turned out to be too complicated, and the useful exhaust too small to solve this problem. Yes, and in our practice we have not yet encountered situations where orthogonal states would necessarily be required, but it would not have been possible to manage, for example, sharing work between several agents tied to one common working context.

    However, if someone needs such a feature, like orthogonal states, is really needed and you have real examples of problems where it is needed, then let's talk. Perhaps, having concrete examples before our eyes, we can add this feature to SObjectizer.

    How support for advanced features of ICA looks in code


    In this part of the story we will try to quickly run through the API SObjectizer-5 to work with ICA. Without a deep immersion in details, just so that the reader has an idea of ​​what is and how it looks. More detailed information, if you wish, can be found in the official documentation .

    Nested states


    In order to declare a nested state, you must pass to the constructor of the corresponding state_t object the expression initial_substate_of or substate_of:
    classdemo :public so_5::agent_t {
      state_t st_parent{this}; // Родительское состояние.state_t st_first_child{initial_substate_of{st_parent}}; // Первое дочернее подсостояние.// К тому же начальное.state_t st_second_child{substate_of{st_parent}}; // Второе дочернее подстостояние.state_t st_third_child{substate_of{st_parent}}; // Третье дочернее подсостояние.state_t st_first_grandchild{initial_substate_of{st_third_child}}; // Еще один уровень вложенности.state_t st_second_grandchild{substate_of{st_third_child]};
      ...
    };

    If state S has several substates C1, C2, ..., Cn, then one of them (and only one) should be marked as initial_substate_of. Violation of this rule is diagnosed at run-time.

    The maximum depth of state nesting in SObjectizer-5 is limited. In versions 5.5 - this is 16 levels. Violation of this rule is diagnosed at run-time.

    The main focus with nested states is that when a state is activated which has nested states, several states are activated at once. Suppose there is a state A, which has substates B and C, and in substate B there are substates D and E:



    When the state A is activated, in fact, three states are activated at once: A, AB and ABD

    The fact that several states can be active at once has the most serious influence on two extremely important things. First, on the search handler for the next incoming message. So, in the example just shown, the handler for the message will first be searched in the ABD state. If there is no suitable handler found, the search will continue in its parent state, i.e. in AB And already affected, if necessary, the search will continue in state A.

    Secondly, the presence of several active states affects the order of calling the input / output handlers for the states. But this will be discussed below.

    I / O Handlers for States


    State can be set to enter and exit state. This is done through the state_t :: on_enter and state_t :: on_exit methods. Usually, these methods are called in the so_define_agent () method (or directly in the agent's constructor, if the agent is trivial and inheritance from it is not provided).
    classdemo :public so_5::agent_t {
      state_t st_free{this};
      state_t st_busy{this};
    ...
      voidso_define_agent() override {
        // Важно: обработчики входа и выхода определяем до того,// как состояние агента будет изменено.
        st_free.on_enter([]{ ... });
        st_busy.on_exit([]{ ...});
        ...
        this >>= st_free;
      }
    ...
    };

    Probably the most difficult moment with on_enter / on_exit handlers is using them for nested states. Let's go back to the example with states A, B, C, D and E.



    Let's assume that each state has an on_enter and an on_exit handler.

    Let the current state of the agent be A. That is. The states A, AB and ABD are activated. During the agent status change process, A.on_enter, ABon_enter and ABDon_enter will be triggered. And in that order.

    Suppose then a transition to ABE occurs: ABDon_exit and ABEon_enter are called.

    If we then transfer the agent to the AC state, then ABEon_exit, ABon_exit, ACon_enter will be called.

    If the agent, being in the AC state, is deregistered, immediately after the so_evt_finish () method completes, the ACon_exit and A.on_exit handlers will be called.

    Time limits


    The time limit for the agent's stay in a particular state is set by the state_t :: time_limit method. As in the case of on_enter / on_exit, time_limit methods are usually called where the agent is configured for its work inside the SObjectizer:
    classled_indicator :public so_5::agent_t {
      state_t inactive{this};
      state_t active{this};
    ...
      voidso_define_agent() override {
        // Разрешаем находится в этом состоянии не более 15s.// По истечении заданного времени нужно перейти в inactive.
        active.time_limit(15s, inactive);
        ...
      }
    ...
    };

    If the time limit for the state is set, then as soon as the agent enters this state, the SObjectizer starts counting the time in the state. If the agent leaves the state and then returns to this state again, then the countdown begins anew.

    If time limits are set for nested states, then you need to be careful, because Curious tricks are possible:
    classdemo :public so_5::agent_t {
      // Состояния верхнего уровня.state_t A{this}, B{this};
      // Вложенные в first состояния.state_t C{initial_substate_of{A}}, st_D{substate_of{A}};
    ...
      voidso_define_agent() override {
        A.time_limit(15s, B);
        C.time_limit(10s, D);
        D.time_limit(20s, C);
        ...
      }
    ...
    };

    Suppose the agent enters the state A. Ie states A and C are activated. Both for A and C, the time begins. Previously, it will end for state C and the agent will go to state D. This will start the countdown for being in state D. But the countdown will continue for A! Since during the transition from C to D, the agent continued to remain in state A. And five seconds after the forced transition from C to D, the agent will go to state B.

    History for the state


    By default, agent states do not have a history. To activate history saving for the state, you need to pass the shallow_history constant to the state_t constructor (the state will have a shallow history) or deep_history (the state will have a deep history). For example:
    classdemo :public so_5::agent_t {
      state_t A{this, shallow_history};
      state_t B{this, deep_history};
      ...
    };

    The history for states is a difficult topic, especially when a decent depth of nesting of states is used and the substates have their own history. Therefore, for more complete information on this topic is better to refer to the documentation , experiment. Well, ask us if you can not figure out yourself;)

    just_switch_to, transfer_to_state, suppress


    The state_t class has a number of the most commonly used methods that have already been shown above: event () for subscribing an event to a message, on_enter () and on_exit () for setting I / O handlers, time_limit () for setting a limit for the time it is in the state.

    Along with these methods, when working with ICA, the following state-t methods are very useful:

    The just_switch_to () method, which is designed for the case when the only reaction to an incoming message is to transfer the agent to a new state. You can write:
    some_state.just_switch_to<some_msg>(another_state);

    instead:
    some_state.event([this](mhood_t<some_msg>) {
      this >>= another_state;
    });

    The transfer_to_state () method is very useful when we have some message M processed in the same way in two or more states S1, S2, ..., Sn. But, if we are in the states S2, ..., Sn, then we first have to return to S1, and only then do the processing of M.

    If this sounds surprising, then perhaps in the example code this situation will be better understood:
    classdemo :public so_5::agent_t {
      state_t S1{this}, S2{this}, ..., Sn{this};
      ...
      voidactual_M_handler(mhood_t<M> cmd){...}
      ...
      voidso_define_agent() override {
        S1.event(&demo::actual_M_handler);
        ...
        // Во всех остальных состояниях мы должны сперва перевести агента в S1,// а уже затем делегировать обработку M реальному обработчику.
        S2.event([this](mhood_t<M> cmd) {
          this >>= S1;
          actual_M_handler(cmd);
        });
        ... // И так для всех остальных состояний.
        Sn.event([this](mhood_t<M> cmd) {
          this >>= S1;
          actual_M_handler(cmd);
        });
      }
    ...
    };

    Here instead of defining very similar event handlers for S2, ..., you can use transfer_to_state:
    classdemo :public so_5::agent_t {
      state_t S1{this}, S2{this}, ..., Sn{this};
      ...
      voidactual_M_handler(mhood_t<M> cmd){...}
      ...
      voidso_define_agent() override {
        S1.event(&demo::actual_M_handler);
        ...
        // Во всех остальных состояниях мы должны сперва перевести агента в S1,// а уже затем делегировать обработку M реальному обработчику.
        S2.transfer_to_state<M>(S1);
        ... // И так для всех остальных состояний.
        Sn.transfer_to_state<M>(Sn);
      }
    ...
    };

    The suppress () method suppresses the search for an event handler for the current substate and all its parent substates. Suppose we have a parent state A, in which std :: abort () is called on message M. And there is a child state B in which M can be safely ignored. We must determine the reaction to M in the B substate, because if we do not do this, then a handler for B will be found in A. Therefore, we will need to write something like:
    voidso_define_agent() override {
      A.event([](mhood_t<M>) { std::abort(); });
      ...
      B.event([](mhood_t<M>) {}); // Сами ничего не делаем, но и не разрешаем искать// обработчик для M в родительских состояниях.
      ...
    }

    The suppress () method allows you to write this situation in the code more clearly and clearly:
    voidso_define_agent() override {
      A.event([](mhood_t<M>) { std::abort(); });
      ...
      B.suppress<M>(); // Сами ничего не делаем, но и не разрешаем искать// обработчик для M в родительских состояниях.
      ...
    }

    Very simple example


    The standard SObjectizer v.5.5 examples include a simple blinking_led example that simulates the operation of a flashing LED indicator. The agent state diagram from this example looks like this:



    Here’s what the full agent code from this example looks like:
    classblinking_ledfinal :public so_5::agent_t
    {
    	state_t off{ this }, blinking{ this },
    		blink_on{ initial_substate_of{ blinking } },
    		blink_off{ substate_of{ blinking } };
    public :
    	structturn_on_off :public so_5::signal_t {};
    	blinking_led( context_t ctx ) : so_5::agent_t{ ctx }
    	{
    		this >>= off;
    		off.just_switch_to< turn_on_off >( blinking );
    		blinking.just_switch_to< turn_on_off >( off );
    		blink_on
    			.on_enter( []{ std::cout << "ON" << std::endl; } )
    			.on_exit( []{ std::cout << "off" << std::endl; } )
    			.time_limit( std::chrono::milliseconds{1250}, blink_off );
    		blink_off
    			.time_limit( std::chrono::milliseconds{750}, blink_on );
    	}
    };

    Here all the actual work is done inside the I / O handlers for the blink_on substate. Well, and plus, the limits for the stay in the blink_on and blink_off substates work.

    Not a very simple example.


    The staff examples of SObjectizer v.5.5 also include a much more complex example, intercom_statechart , which simulates the behavior of an intercom panel. And the state diagram of the main agent in this example looks approximately like this:



    Everything is so harsh because this imitation supports not only the call for an apartment by number, but also such things as a unique secret code for each apartment, as well as special service code. These codes allow you to open the door lock without dialing anywhere.

    There are more interesting things in this example. But it is too big to paint it in detail (even a separate article may not be enough for that). So if it’s interesting how really complex ICAs look in SObjectizer, you can see in this example. And if something is not clear, then you can ask us a question. For example, in the comments to this article.

    Is it possible not to use the spacecraft support built into SObjectizer-5?


    So, in SObjectizer-5 there is built-in support for ICA with a very wide range of supported features. This support is made, of course, in order to use it. In particular, the SObjectizer debugging mechanisms, like message delivery tracing , are aware of the agent states and display the current states in their respective debug messages.

    However, if a developer does not want for some reason to use the built-in tools of SObjectizer-5, then he may not.

    For example, you can refuse to use SObjectizer-ovsky state_t and others like it because state_t is a rather heavy object that has std :: string, a pair of std :: function, and several counters of type std :: size_t, five pointers to various objects and some other trifle. All together, this on 64-bit Linux and GCC-5.5, for example, gives 160 bytes per state_t (not counting what can be placed in dynamic memory).

    If your application requires, say, a million agents, each of which will have 10 states, then the overhead of SObjectizer state_t may be unacceptable. In this case, you can use any other mechanism for working with finite-state machines, manually delegating the processing of messages to this mechanism. Sort of:
    classexternal_fsm_demo :public so_5::agent_t {
      some_fsm_type my_fsm_;
      ...
      voidso_define_agent() override {
        so_subscribe_self()
            .event([this](mhood_t<msg_one> cmd) { my_fsm_.handle(*cmd); })
            .event([this](mhood_t<msg_two> cmd) { my_fsm_.handle(*cmd); })
            .event([this](mhood_t<msg_three> cmd) { my_fsm_.handle(*cmd); });
        ...
      }
      ...
    };

    In this case, you are paying for efficiency by increasing the amount of manual work and the lack of help from the debugging mechanisms of SObjectizer. But here the developer is to decide.

    Conclusion


    The article turned out to be voluminous, much more than originally planned. Thanks to everyone who read to this place. If any readers find it possible to leave their feedback in the comments to the article, then it will be great.

    If something remains unclear, then ask questions, we will be happy to answer.

    Also taking this opportunity, I would like to draw the attention of those who are interested in SObjectizer, that work has begun on the next version of SObjectizer, as part of the 5.5 branch. A summary of what is considered for implementation in 5.5.23 is described here . More fully, but in English, here. You can leave your opinion on any of the features offered for implementation, or suggest something else. Those. There is a real opportunity to influence the development of SObjectizer. Moreover, after the release of v.5.5.23 it is possible to pause in the work on SObjectizer and the next opportunity to include in SObjectizer something useful in the 2018th may not be.

    Also popular now: