вторник, 12 января 2016 г.

[prog.sobjectizer] Адекватность связки time_limit/drop_time_limit для состояний агентов

В новой версии SO-5.5.15 реализуется поддержка иерархических конечных автоматов (об этом уже писалось). В процессе реализации оказалось, что для нормальных ИКА нужно практически все, что было описано David Harel в его основополагающей работе "Statecharts: A Visual Formalism For Complex Systems" и затем перекочевало в диаграммы состояний UML. Например: композитные состояния, обработчики входа-выхода, состояния с историями. И, среди прочего, автоматическая смена состояния после прошествия указанного времени.

В SO-5.5.15 поддержка автоматической смены состояния агента после указанного тайм-аута реализуется посредством метода time_limit для состояния агента.

В качестве демонстрации представим себе простого агента который, скажем, имитирует экран инфокиоска. У агента всего два состояния. Первое состояние dialog. В этом состоянии агент воспринимает действия пользователя (нажатие на экран, нажатия на кнопки и т.д.) и отображает какую-то информацию. Второе состояние show_err. Это состояние используется дабы вывести на экран киоска на 2 секунды какое-то сообщение об ошибке. В течении этого времени инфокиоск должен игнорировать любые действия пользователя.

На SO-5.5.15 сейчас это можно представить вот в таком виде:

class basic_infokiosk : public so_5::agent_t
{
protected :
  state_t dialog{ this"dialog" };
  state_t show_err{ this"err" };
  ...
public :
  virtual void so_define_agent() override
  {
    // Нормальный диалог с пользователем.
    dialog
      // На входе в состояние очищаем экран.
      .on_enter( [this] { m_display.clear(); } )
      // Обработчик очередного действия пользователя.
      .event( [this]( const user_input & msg ) {
          // Пытаемся что-то сделать.
          try_handle_input( msg );
          if( some_error() ) {
            // Если не получилось, то формируем описание ошибки
            // и идем в состояние show_err для того, чтобы пользователь
            // его прочитал и начал заново.
            m_error = error_description();
            this >>= show_err;
          }
        } );

    // Отображение сообщения об ошибке.
    show_err
      // На входе в состояние отображаем на экране текст ошибки.
      .on_enter( [this] { m_display.show_error_message( m_error ); } )
      // Ограничиваем свое пребывание в этом состоянии всего двумя
      // секундами после чего возвращаемся в нормальное состояние.
      .time_limit( std::chrono::seconds{2}, dialog );
  }
  ...
protected :
  display m_display;
  std::string m_error;
  ...
};

Даже на тех небольших примерах, на которых обкатывалась реализация ИКА, оказалось, что time_limit для состояний очень удобен: пользователь освобождается от необходимости вручную работать с отложенными сообщениями, их отменой, проверкой уникальности и т.д. В общем, для более-менее сложных ИКА time_limit, как сейчас представляется, -- это must have feature.

В пару к time_limit для состояния был добавлен еще и метод drop_time_limit, который позволяет отменить заданный для состояния лимит времени.

В принципе, drop_time_limit добавлялся из соображений "шоб было". Т.е., если есть возможность задать временной лимит, то может потребоваться и возможность от этого лимита избавиться. Да и сценарий, где drop_time_limit может потребоваться, придумать не так уж и сложно. Допустим, мы используем наследование агентов. И кроме класса basic_infokiosk, который отображает сообщение об ошибке в течении 2-х секунд, у нас есть и его наследник, adv_infokiosk, который во время отображения ошибки позволяет пользователю выбрать какое-то корректирующее действие, ввести дополнительные параметры и т.д. Т.е. класс-наследник в show_err ведет свой диалог с пользователем и наследнику не нужно это ограничение на 2 секунды. Наследник может полностью отменить ограничение посредством drop_time_limit:

// Более продвинутая версия, которая не нуждается в ограничении на время
// пребывания в show_err.
class adv_infokiosk : public basic_infokiosk
{
public :
  virtual void so_define_agent() override
  {
    // Позволяем базовому классу провести настройку своего поведения.
    basic_infokiosk::so_define_agent();

    // Меняем свойства состояния show_err.
    show_err
      // Лимит на время нахождения в состоянии больше не нужен.
      .drop_time_limit()
      // Какие-то действия для состояния show_err...
      .event...;
  }
  ...
};

До сих пор все было более-менее просто и, надеюсь, понятно. Дальше веселее ;)

Метод drop_time_limit для состояния S можно вызывать когда агент уже находится в этом самом состоянии S. После чего автоматический переход в другое состояние отменяется полностью. Т.е. если агента из его текущего состояния S не вывести вручную, автоматически он уже никуда не перейдет.

Где это может потребоваться? Да в том же примере с инфокиоском.

Допустим, у нас есть еще один наследник basic_infokiosk, который отображает сообщение об ошибке и показывает кнопку "Подробнее...". Если пользователь ничего не делает, то выход из show_err происходит через 2 секунды. Если нажимает "Подробнее...", то нужно отобразить расширенное описание ошибки. Показывать это описание нужно не более 30 секунд.

В коде это может выглядеть вот так:

// Еще одна продвинутая версия инфокиоска, которая позволяет получить
// расширенную информацию об ошибке.
class extended_infokiosk : public basic_infokiosk
{
  // Пара новых состояний, которые нам потребуются дабы расширить
  // поведение агента в состоянии show_err.
  state_t err_default{ initial_substate_of{ show_err }, "default" };
  state_t err_extended{ substate_of{ show_err }, "extended" };

public :
  virtual void so_define_agent() override
  {
    // Позволяем базовому классу провести настройку своего поведения.
    basic_infokiosk::so_define_agent();

    // Расширяем логику поведения агента посредством расширения
    // поведения агента в состоянии show_err.
    // Не забываем: базовый класс назначил ограничение на пребывание
    // в состоянии show_err не более 2-х секунд.
    show_err
      // Реакция на нажатие "Подробнее..."
      .event< more_info >( [this] {
            // Отменяем отсчет лимита времени для show_err.
            show_err.drop_time_limit();

            // Идем в состояние err_extended для того, чтобы отобразить
            // расширенное описание ошибки.
            this >>= err_extended;
          } );

    // Определяем поведение агента в новом состоянии.
    err_extended
      // На входе в состояние отображаем дополнительную информацию.
      .on_enter( [this] { m_display.show_extended_error( make_extended_info(); } )
      // Ограничиваем время пребывания в этом состоянии 30 секундами.
      .time_limit( std:chrono::seconds{30}, dialog )
      // По нажатию на "OK" возвращаемся к нормальному диалогу.
      .just_switch_to< ok >( dialog );
  }
  ...
};

Все бы ничего, но! Когда мы войдем в show_err в следующий раз, то ограничения на время пребывания в show_err уже не будет. Ведь оно было отменено вызовом drop_time_limit. Поэтому, нам нужно где-то вызывать time_limit для show_err еще раз.

И вот тут проявляется особенность, которая существует в текущей реализации SO-5.5.15: если агент находится в состоянии S и последовательно вызывает S.drop_time_limit, затем S.time_limit, то заданный таким образом лимит времени будет активирован для состояния S только при следующем входе в состояние S.

Это означает, что если мы сбросили лимит времени для show_err, вошли в show_err, установили лимит времени заново, то автоматически из show_err по истечении этого лимита мы не выйдем. Т.к. отсчет лимита времени начинается при входе в состояние. Если на момент входа в show_err лимита времени не было, то ничего не отсчитывается.

Вот такая особенность есть сейчас. И обходится она, например, вот так:

show_err
  // Реакция на нажатие "Подробнее..."
  .event< more_info >( [this] {
        // Отменяем отсчет лимита времени для show_err.
        show_err.drop_time_limit();
        // И назначаем его снова, дабы он отсчитывался при
        // следующем входе в show_err.
        show_err.time_limit( std::chrono::seconds{2}, dialog );

        // Идем в состояние err_extended для того, чтобы отобразить
        // расширенное описание ошибки.
        this >>= err_extended;
      } );

Либо вот так:

show_err
  // По нажатию на "Подробнее..." идем в специальное подсостояние,
  // где и будет отображаться расширенная информация об ошибке.
  .just_switch_to< more_info >( err_extended );

// Определяем поведение агента в новом состоянии.
err_extended
  // На входе в состояние отображаем дополнительную информацию.
  // А так же отменяем лимит времени пребывания в show_err.
  // Нам нужна эта отмена, т.к. состояние err_extended является подстостоянием show_err.
  .on_enter( [this] {
        show_err.drop_time_limit();
        m_display.show_extended_error( make_extended_info();
      } )
  // На выходе из состояния восстанавливаем лимит на время
  // пребывания в состоянии show_err.
  .on_exit( [this] {
        show_err.time_limit( std::chrono::seconds{2}, dialog );
      } )
  // Ограничиваем время пребывания в этом состоянии 30 секундами.
  .time_limit( std:chrono::seconds{30}, dialog )
  // По нажатию на "OK" возвращаемся к нормальному диалогу.
  .just_switch_to< ok >( dialog );

Когда делалась текущая реализация лимитов времени для состояния, видимо, об этом просто не подумал. А когда стал писать документацию, наткнулся на такую особенность. И мне показалось, что подобное поведение для пользователей SO-5 может стать откровением.

Посему интересно мнение читателей. А что для вас было бы более логичным:

  1. Вызов S.time_limit когда агент уже находится в состоянии S сразу же начинает отсчет лимита времени для S (повторный вход в S не требуется);
  2. Вызов S.time_limit когда агент уже находится в состоянии S не начинает отсчет лимита времени для S (лимит времени будет отсчитываться только при повторном входе в S).

Есть ли у кого-нибудь какие-нибудь мнения? Или все это уже слишком мудрено?

Upd. В итоге реализация time_limit изменена: если при вызове S.time_limit() состояние S уже активно (само по себе или активно какое-то из его подсостояний), то отсчет времени пребывания агента в состоянии S начинается заново. Повторный вход в S для срабатывания временного лимита не нужен.


Кстати говоря, даже на таком тривиальном примере с классами *_infokiosk может стать заметно, что дружба модели акторов и ООП имеет некоторые специфические особенности. Поэтому тупой перенос реализации модели акторов из Erlang-а в C++ вряд ли имеет смысл. Тут нужно тоньше. Ну или толще :)

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