четверг, 14 января 2016 г.

[prog.sobjectizer] Состояния конечного автомата с историей -- это прикольно

Конечные автоматы с состояниями, у которых есть история -- это прикольная штука. Пока с этим не сталкиваешься, то думаешь: "Ну а что в этом такого?" А вот когда сталкиваешься... Тогда думаешь, что получить такой же эффект какими-то другими средствами было бы, наверное, совсем не просто.

Для начала немного вводной информации для тех, кто раньше про историю состояний не слышал.

История у состояний бывает двух типов: shallow и deep (эти термины гораздо лучше, чем русскоязычные "поверхностная" или "глубокая" история). Shallow-history хранит информацию только об одном уровне активных подсостояний. Например, если есть состояние A с shallow-history и с подсостояниями B, C и D, то A будет помнить лишь о том, какое из его непосредственных подсостояний было активно в последний раз: B, С или D. Если, скажем, в подсостоянии B есть еще несколько подсостояний (B1, B2,...), то активность этих состояний в истории состояния A не отражается. Т.е., если произошел переход в состояние B1, то в истории A будет отражена только активность состояния B, но не B1.

В случае же deep-history для A в истории будет сохраняться информация о любом активном подсостоянии A, вне зависимости от того, на какой глубине вложенности оно находится. Так, если у A есть подсостояние B, а у B есть подсостояние B1, то при переходе в B1 в истории A будет сохранена информация об активности B1.

История используется при повторном входе в состояние. Допустим, мы входим в состояние B1 (можно сказать, что мы вошли в состояние A.B.B1), затем переходим в какое-то состояние X. После чего возвращаемся в состояние A. В случае с shallow-history мы автоматически попадем в состояние B (т.е. в A.B). В случае с deep-history мы автоматически попадем в состояние B1 (т.е. в A.B.B1). Временами такое восстановление состояния по истории очень удобно, т.к. дает возможность продолжить работу конечного автомата с той точки, в которой эта работа была приостановлена.

В качестве демонстрации истории состояний в новую версию SO-5.5.15 включен пример state_deep_history, в котором задействуется состояние с deep-историей. Он так же эксплуатирует имитацию домофона, но в данном случае акцент сделан на контроль того, что пользователь вводит в консоли домофона.

Итак, пользователь может:

  • ввести последовательность "dddB" (т.е. три цифры, затем кнопка "B" (Bell, т.е. звонок)), эта последовательность должна инициировать звонок в квартиру с номером "ddd";
  • ввести последовательность "#ddd#ddddB" (т.е. кнопка "#", затем три цифры номера квартиры, затем "#", затем четыре цифры секретного кода этой квартиры, затем "Bell"), эта последовательность отпирает дверь подъезда при правильном вводе секретного кода;
  • ввести последовательность "##ddddd#" (т.е. два раза кнопка "#", затем пять цифр сервисного кода домофона, затем еще раз "#"), эта последовательность отпирает дверь подъезда при правильном вводе сервисного кода.

Пользователь может ошибаться. Например, ввести две цифры номера квартиры вместо трех или набрать последовательность "#dddB", которая не является разрешенной. При таких ошибках нужно вывести сообщение, пока это сообщение отображается, работа с консолью блокируется.

Для демонстрации этой задачи используется небольшой КА, который выглядит приблизительно вот так:

Пожалуй, единственная важная поправочка к рисунку -- это отсутствие переходов в состояние show_error. Эти переходы выполняются из многих подсостояний состояния dialog, но я их не показывал, дабы не усложнять схему множеством однотипных линий.

Можно заметить, что состояние dialog помечено специальным маркером H*. Этот маркер указывает, что состояние dialog -- это состояние с deep-history. Т.е. dialog будет помнить о том, какое из его подсостояний, вне зависимости от вложенности, было активно в последний раз.

Наличие deep-history у состояния dialog делает работу описанного выше КА тривиальной. Когда в каком-то из подсостояний dialog обнаруживается допущенная пользователем ошибка, происходит переход в состояние show_error. В состоянии show_error описание ошибки отображается на экране, а сама консоль блокируется на 2 секунды (нажатия на кнопки игнорируются). Спустя 2 секунды происходит возврат из show_error в dialog. А т.к. у dialog-а есть deep-history, то возврат производится именно в то подсостояние, из которого мы ушли. Например, если ошибка была обнаружена внутри состояния user_code_apartment_number, то и возврат произойдет именно в него.

Все, что описано выше, уже работает. Полный текст примера можно увидеть в репозитории. Здесь же покажу всего два фрагмента, относящихся к описанию КА средствами SO-5.5.15.

Первый фрагмент -- это декларация состояний, которые входят в данный КА:

class console final : public so_5::agent_t
{
   state_t
      dialog{ this"dialog", deep_history },

         wait_activity{
               initial_substate_of{ dialog }, "wait_activity" },
         number_selection{ substate_of{ dialog }, "number_selection" },

         special_code_selection{
               substate_of{ dialog }, "special_code_selection" },

            special_code_selection_0{
                  initial_substate_of{ special_code_selection },
                  "special_code_selection_0" },

            user_code_selection{
                  substate_of{ special_code_selection },
                  "user_code_selection" },
               user_code_apartment_number{
                     initial_substate_of{ user_code_selection },
                     "apartment_number" },
               user_code_secret{
                     substate_of{ user_code_selection },
                     "secret_code" },

            service_code_selection{
                  substate_of{ special_code_selection },
                  "service_code" },


         operation_completed{ substate_of{ dialog }, "op_completed" },

      show_error{ this"error" }
   ;

Второй фрагмент -- это декларация того, что будет обрабатываться в каждом из состояний:

console( context_t ctx ) : so_5::agent_t{ ctx }
{
   dialog
      .event( &console::dialog_on_grid )
      .event( &console::dialog_on_cancel );

   wait_activity
      .on_enter( &console::wait_activity_on_enter )
      .transfer_to_state< key_digit >( number_selection );

   number_selection
      .on_enter( &console::apartment_number_on_enter )
      .event( &console::apartment_number_on_digit )
      .event( &console::apartment_number_on_bell )
      .event( &console::apartment_number_on_grid );

   special_code_selection_0
      .transfer_to_state< key_digit >( user_code_selection )
      .just_switch_to< key_grid >( service_code_selection );

   user_code_apartment_number
      .on_enter( &console::user_code_apartment_number_on_enter )
      .event( &console::apartment_number_on_digit )
      .event( &console::user_code_apartment_number_on_bell )
      .event( &console::user_code_apartment_number_on_grid );

   user_code_secret
      .on_enter( &console::user_code_secret_on_enter )
      .event( &console::user_code_secret_on_digit )
      .event( &console::user_code_secret_on_bell )
      .event( &console::user_code_secret_on_grid );

   service_code_selection
      .on_enter( &console::service_code_on_enter )
      .event( &console::service_code_on_digit )
      .event( &console::service_code_on_bell )
      .event( &console::service_code_on_grid );

   operation_completed
      .on_enter( &console::op_completed_on_enter )
      .time_limit( std::chrono::seconds{3}, wait_activity );

   show_error
      .on_enter( &console::show_error_on_enter )
      .on_exit( &console::show_error_on_exit )
      .time_limit( std::chrono::seconds{2}, dialog );
}

В этих фрагментах, пожалуй, собрано все, что есть в SO-5.5.15 для поддержки иерархических КА: простые и композитные состояния, история для состояний, on_enter/on_exit, transfer_to_state и just_switch_to, а так же time_limit. Имхо, для первого приближения вполне достатояно. Тем более, что по некоторым вещам из области ИКА не понятно, что делать. Надеюсь, что какие-то дополнительные идеи появятся уже после релиза версии 5.5.15 и их реализация войдет в последующие версии.

Напоследок покажу еще кусочек трейса работы программы, по которому видно, как доставляются сообщения и как агент переходит из одного состояния в другое (трейс получен обычными шатными средствами механизма msg_tracing):

[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.wait_activity]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_grid][signal][state=dialog.wait_activity][evt_handler=0x1fdf009d3a0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.wait_activity]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.special_code_selection_0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a8340][payload_ptr=0x1fdf00a8350][state=dialog.special_code_selection.special_code_selection_0][evt_handler=0x1fdf009d550]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.special_code_selection.special_code_selection_0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.user_code_selection.apartment_number]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a8340][payload_ptr=0x1fdf00a8350][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d5e0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_digit][envelope_ptr=0x1fdf00a84c0][payload_ptr=0x1fdf00a84d0][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d5e0]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=4][msg_type=struct key_bell][signal][state=dialog.special_code_selection.user_code_selection.apartment_number][evt_handler=0x1fdf009d790]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=dialog.special_code_selection.user_code_selection.apartment_number]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=error]
[tid=6948][agent_ptr=0x1fdf00ae5d0] demand_handler_on_message.find_handler [mbox_id=5][msg_type=struct so_5::state_t::time_limit_t::timeout][signal][state=error][evt_handler=0x1fdf009dd30]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.leaving [state=error]
[tid=6948][agent_ptr=0x1fdf00ae5d0] state.entering [state=dialog.special_code_selection.user_code_selection.apartment_number]

Комментариев нет: