четверг, 23 марта 2017 г.

[prog] А кому нужны типизированные агенты?

В комментариях ко вчерашней статье на Habr-е очень активно обсуждается тема типизированных акторов. Мол, проблема акторов в том, что они "нетипизированны", ты отсылаешь актору сообщение A, но не знаешь, может ли этот актор обрабатывать сообщения типа A или же он ждет сообщения типа B. А вот если бы акторы были типизированными, то мы бы имели не просто actor_reference, а actor_reference<B> и во время компиляции бы получили по рукам за попытку отослать актору сообщение типа A.

Признаться, я удивлен. По моему опыту, проблемы с типизацией сообщений (т.е. когда агенту по ошибке отсылается сообщение не того типа) -- это какая-то дивная экзотика. Гораздо чаще встречаются ситуации, когда агент не подписывается на сообщение, которое должен был бы обрабатывать. Ну вот должен был подписаться, но программист забыл это сделать. Еще более частая проблема -- это когда подписка на сообщение делается в одном состоянии, но не делается в другом.

Однако, это мой опыт, он не может служить адекватным мерилом. И раз люди о такой проблеме говорят, то возникает желание добавить в SO-5 дополнительную функциональность для того, чтобы удовлетворить еще и сторонников статической типизации. Но т.к. SO-5 уже не такой маленький проект и каждая новая фича, даже самая маленькая, увеличивает стоимость его развития и поддержки, то хочется понять для себя: а стоит ли овчинка выделки?

Посему вопрос: а кому хотелось бы иметь акторов с типизированными интерфейсами в фреймворках вроде Akka, CAF, SObjectizer, QP/C++ и пр.?

Под катом маленький пример того, как это может выглядеть в SObjectizer...

// Типы сообщений, которые готов обрабатывать агент request_processor.
struct request {...};
struct get_status {...};
struct reconfigure {...};

// Сам агент request_processor
class request_processor : public so_5::agent_t {
   // Типизированный почтовый ящик для ограниченного набора типов сообщений.
   const so_5::typed_mbox_t<request, get_status, reconfigure> self_mbox_;

   // Еще один тип сообщения, который должен обрабатываться данным агентом.
   struct checkpoint {...};
public :
   request_processor(context_t ctx)
      :  so_5::agent_t(std::move(ctx))
      // Сразу создаем типизированный почтовый ящик.
      ,  self_mbox_(so_environment().create_typed_mbox<request, get_status, reconfigure>())
   {
   }

   // Доступ к типизированному почтовому ящику, чтобы можно было
   // раздавать mbox агента всем партнерам.
   auto self_mbox() const { return self_mbox_; }

   virtual void so_define_agent() override {
      // При подписках будет контролироваться, входит ли тип сообщения,
      // на который происходит подписка, в список типов типизированного 
      // почтового ящика. Если не входит, то должна быть ошибка времени
      // компиляции.
      so_subscribe(self_mbox_)
         // Тут все должно быть нормально.
         .event([this](const request & cmd){...})
         // И тут все должно быть нормально.
         .event([this](const reconfigure & cmd){...})
         // А вот тут должна возникнуть ошибка времени компиляции,
         // т.к. тип checkpoint не входит в список типов self_mbox_.
         .event([this](const checkpoint & cmd){...});
   }
};

// Тип агента-партнера, который должен общаться с request_processor-ом
// только посредством сообщений get_status и reconfigure.
class processor_observer : public so_5::agent_t {
   // Почтовый ящик агента-партнера.
   // Можно увидеть, что здесь только подмножество типов сообщений,
   // которые способен обрабатывать request_processor.
   const so_5::typed_mbox_t<get_status, reconfigure> processor_;
public :
   processor_observer(context_t ctx,
      so_5::typed_mbox_t<get_status, reconfigure> processor) {...}

   ...
   void on_some_event() {
      // В какой-то момент времени мы захотели отослать get_status
      // нашему request_processor-у. Этот вызов send-а компилятор
      // пропустит.
      so_5::send<get_status>(processor_, ...);

      // А вот этот -- нет, т.к. типа turn_logging_off в списке типов
      // сообщений для нашего типизированного mbox-а нет.
      so_5::send<turn_logging_off>(processor_, ...);
      ...
   }
};

// Еще один тип агента-партнера, который должен общаться с request_processor-ом
// только посредством сообщений request.
class request_loader : public so_5::agent_t {
   // Еще один типизированный почтовый ящик, но уже со своим очень
   // коротким списком типов.
   const so_5::typed_mbox_t<request> processor_;
public :
   request_loader(context_t ctx,
      so_5::typed_mbox_t<request> processor) {...}
   ...
   void on_some_event() {
      ... // Подготовка очередного запроса для обработки.
      // Отсылка запроса. Этот send будет разрешен компилятором.
      so_5::send<request>(processor_, ...);
      
      // А вот этот -- нет.
      so_5::send_delayed<get_status>(processor_, ...);
   }
};

// Создание агентов, которые должны работать совместно.
env.introduce_coop([](so_5::coop_t & coop) {
   // Первым создаем request_processor-а, т.к. его mbox нужен будет
   // всем последующим агентам.
   auto processor_agent = coop.make_agent<request_processor>();

   // Забираем mbox, который request_processor выставляет наружу.
   // Тип этого mbox-а -- so_5::typed_mbox_t<request, get_status, reconfigure>;
   const auto processor_mbox = processor_agent->self_mbox();

   // Создаем остальных агентов.
   coop.make_agent<processor_observer>(
      // Здесь произойдет автоматическая конвертация из
      // so_5::typed_mbox_t<request, get_status, reconfigure> в
      // so_5::typed_mbox_t<get_status, reconfigure>.
      processor_mbox);
   coop.make_agent<request_loader>(
      // Здесь произойдет автоматическая конвертация из
      // so_5::typed_mbox_t<request, get_status, reconfigure> в
      // so_5::typed_mbox_t<request>.
      processor_mbox);
});

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