Нужен ли SObjectizer-у DSL?

Author: Евгений Охотников
Contact: eao197 at intervale dot ru; eao197 at yahoo dot com
Date: 2007.07.22

Contents

Преамбула

SObjectizer-4 не использовал внешних языков для описания классов агентов поскольку первая версия SObjectizer-4 создавалась в очень сжатые сроки и на разработку специализированного языка (DSL -- Domain Specific Language) просто не было времени. Вероятно, на тот момент это было правильное решение, которое позволило получить работающий SObjectizer и применить его в реальных проектах.

По прошествии пяти лет использования SObjectizer-4 возникает вопрос о то, каким будет SObjectizer-5. Возможно, следующие версии SObjectizer либо будут серьезно переделаны по сравнению с SObjectizer-4, либо вообще будут созданы для других языков программирования (D/C#/Java/Scala/Eiffel, конкретный язык еще не выбран). Поэтому есть смысл вернуться к идее внешнего DSL для SObjectizer.

Для чего DSL?

Сейчас DSL может использоваться для описания класса агента. Так, вместо макросов:

SOL4_CLASS_START( a_creature_t )
  SOL4_EVENT( evt_start )
  SOL4_EVENT_STC( evt_meet_completed,
      a_meeting_place_t::msg_meet_completed )

  SOL4_STATE_START( st_normal )
    SOL4_STATE_EVENT( evt_start )
    SOL4_STATE_EVENT( evt_meet_completed )
  SOL4_STATE_FINISH()
SOL4_CLASS_FINISH()

возможно более компактное описание:

{agent_class a_creature_t
  {event evt_start}
  {event evt_meet_completed
      {by msg_meet_completed {of a_meeting_place_t} } } }

  {state st_normal
    {events
        evt_start
        evt_meet_completed } }
}

Это описание помещается в отдельный файл (например, a_creature.sol5) и обрабатывается специальным транслятором DSL. На выходе транслятора получается набор hpp/cpp файлов, которые подключаются в основной исходный текст через директивы #include. (В случае отличных от C++ языков программирования на выходе могут быть какие-нибудь классы/интерфейсы, которые содержат вспомогательный код для SObjectizer-а).

Какие выгоды мог бы дать DSL?

Упрощение SObjecizer Run-Time

Сейчас SObjectizer Run-Time вынужден обрабатывать динамическое изменение состава классов агентов и их взаимосвязей. Поскольку за макросами SOL4_* скрываются глобальные переменные, которые в собственных конструкторах передают в системный словарь SObjectizer-а описания классов агентов. Возможность динамической загрузки/выгрузки DLL с агентами приводит к ситуации, когда SObjectizer должен реагировать на появление и исчезновения классов агентов. Что особенно сложно при обработке наследования для агентов и мержинге состояний агентов.

При наличии же DSL эту работу может выполнять транслятор DSL. Так, если агент использует наследование и мержинг состояний, то все конфликты будут выявлены еще при компиляции, а не при запуске приложения. Для этого при описании в DSL класса-наследника нужно будет сделать для транслятора доступными описания базовых классов. Например, такое описание:

SOL4_CLASS_START( a_derived_t )

  SOL4_SUPER_CLASS( a_base_t )
  SOL4_INITIAL_STATE( st_normal )

  SOL4_MSG_START( msg_2, a_derived_t::msg_2 )
  SOL4_MSG_FINISH()

  SOL4_EVENT( evt_msg_2 )

  SOL4_STATE_START( st_normal )
    SOL4_STATE_MERGE( a_base_t, st_normal )

    SOL4_STATE_EVENT( evt_msg_2 )
  SOL4_STATE_FINISH()

SOL4_CLASS_FINISH()

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

{include "a_base.sol5" }

{agent_class a_derived_t
  {super a_base_t }
  {initial_state st_normal }

  {message msg_2 {of a_derived_t::msg_2} }

  {event evt_msg_2}

  {state st_normal
    {merge {with st_normal} {from a_base_t}}
    {events evt_msg_2}
  }
}

Поскольку транслятор DSL будет иметь всю необходимую информацию, то в сгенерированном вспомогательном коде будет находиться полностью готовое описание класса агента для SObjectizer. Это описание уже не нужно будет изменять во время работы SObjectizer.

Таким образом из SObjectizer Run-Time изрядный кусок работы будет перемещен в транслятор DSL. Что благоприятно скажется на объеме и сложности ядра SObjectizer. Например, транслятор DSL может быть написан на одном языке программирования (Ruby, к примеру), а ядро SObjectizer может быть реализовано для ряда других языков (C++, D, Scala).

Генерация вспомогательных классов/методов/объектов для сообщений/событий/состояний

Справедливую критику в SObjectizer вызывает то, что для идентификации сообщений, событий и состояний в SObjectizer используются строки. Хотя строки зарекомендовали себя как очень удобный подход (в частности, имена состояний и сообщений можно формировать во время исполнения, имена сообщений можно передавать через SOP из-вне приложения), все же хочется иметь статический контроль компилятора. Достичь этого можно, если транслятор DSL будет генерировать специальные вспомогательные классы/методы/объекты, являющиеся, по сути, обертками над текстовыми именами.

Например, если в DSL описании указано:

{agent_class a_meeting_place_t
  {message msg_creature_in {of ... } }
  {message msg_meet_completed {of ...} }
  ...

  {event evt_first_in {by msg_creature_in {of {self}}} }
  {event evt_second_in {by msg_creature_in {of {self}}} }
  ...

  {state st_empty {events evt_first_in}}
  {state st_first_in {events evt_second_in}}
  ...
}

то в коде агента a_meeting_place_t можно будет использовать следующие имена:

void a_meeting_place_t::so_on_subscription()
  {
    // Подписка событий на сообщения.
    so_events().evt_first_in().subscribe(
        so_messages().msg_creature_in() );
    so_events().evt_second_in().subscribe(
        so_messages().msg_creature_in() );
  }

void a_meeting_place_t::evt_first_in( const msg_creature_in & cmd )
  {
    ...
    // Переход в состояние.
    so_states().st_first_in().switch_to();
  }

void a_meeting_place_t::evt_second_in( const msg_creature_in & cmd )
  {
    ...
    // Отсылка сообщения.
    auto_ptr< msg_meet_completed > msg1 =
        so_messages().msg_meet_complete().make();
    msg1->... // Инициализация.
    msg1->send();
    ...
    // Переход в состояние.
    so_states().st_empty().switch_to;
  }

в грядущем C++09 или в D/Scala (в общем, если доступен вывод типов) работа с сообщениями может стать еще компактнее:

// Например, в D.
auto msg1 = soMessages.msgMeetComplete.make;
...
msg.send;

Главное достоинство этого подхода в том, что изменение имени сообщения/события/состояния в DSL автоматически сделает исходный код некомпилирируемым. И компилятор сам будет указывать разработчику, какие фрагменты необходимо переделать.

Реализация части процедуры подписки

В подавляющем количестве случаев заранее известно, на чьи сообщения агент будет подписываться (в многих случаях это его собственные сообщения). И подписка не изменяется в течении жизни агента. Поэтому, если предоставить возможность в DSL перечислить имена инцидентов событий, то код so_on_subscription может быть сгенерирован транслятором DSL:

{agent_class a_meeting_place_t
  {message msg_creature_in {of ... } }
  {message msg_meet_completed {of ...} }
  {message msg_creature_report {of ...} }
  ...

  {event evt_first_in {by msg_creature_in {of {self}}} }
  {event evt_second_in {by msg_creature_in {of {self}} }
  {event evt_creature_report {by msg_creature_report {of {self}}} }
  ...
}

{agent_class a_creature_t
  {event evt_start {by msg_start {of {sobjectizer_agent}}}}
  {event evt_meet_completed
    {by msg_meet_completed {of a_meeting_place_t}
        {owner_name_from_expression "m_meeting_place_name"}}}
  ...
}

что приведет к:

void a_meeting_place_t::so_on_subscription()
  {
    so_events().evt_first_in().subscribe(
        so_query_name(),
        so_messages().msg_creature_in() );
    so_events().evt_second_in().subscribe(
        so_query_name(),
        so_messages().msg_creature_in() );
    so_events().evt_creature_report().subscribe(
        so_query_name(),
        so_messages().msg_creature_report() );
  }

void a_creature_t::so_on_subscription()
  {
    so_events().evt_start().subscribe(
        so_4::rt::sobjectizer_agent_name(),
        so_4::rt::sobjectizer_agent_t::so_messages().msg_start() );
    so_events().evt_meet_completed.subscribe(
        m_meeting_place_name,
        a_meeting_place_t::so_messages().msg_meet_completed() );
  }

Естественно, что будет предложен какой-то способ разбавить автоматически генерируемый so_on_subscription кодом разработчика для случаев, когда детали подписки выясняются только в процессе работы приложения.

Генерация части кода, который сейчас пишется вручную

Сейчас для агента нужно сделать полностью корректное описание C++ класса агента, после чего нужно продублировать часть этого описания в макросах SOL4_*. Но часть этой работы может сделать транслятор DSL. Так, если все события описаны в DSL, то вместо ручного описания прототипов обработчиков событий в hpp-файле можно будет в тело C++ класса подставить инструкцию #include:

class a_meeting_place_t : public so_4::rt::agent_t
  {
  #include "a_meeting_place.sol5.hpp"
  ...
  };

Упрощение декларации связанных с SObjectizer деталей для не-C++ реализаций

В C++ можно было скрывать довольно сложные конструкции за макросами. В D/Java/Scala/C#/Eiffel макросов нет. Поэтому для них DSL представляется чуть ли не единственным удобным способом описания деталей агентов для SObjectizer-а.

Например, для D транслятор DSL может генерировать фрагмент кода, который разработчик подключает в тело класса агента через mixin:

class AMeetingPlace : so5.rt.Agent
  {
    // Подключение как шаблона.
    mixin AMeetingPlace_SObjPart;

    // Или подключение как текстового mixin-а.
    mixin( import( "a_meeting_place.sol5.d" ) );
  }

Для Scala можно генерировать trait-ы, который будет подмешиваться к классу агента:

class AMeetingPlace extends so5.rt.Agent with AMeetingPlace_SObjPart
  { ... }

Аналогично для Eiffel-я можно будет генерировать вспомогательный класс, который будет наследоваться классом агента:

class A_MEETING_PLACE inherit
  AGENT
  A_MEETING_PLACE_SOBJ_PART
feature
  ...
end

Для C# 2.0, может быть, получится использовать т.н. partial classes.

Примечания

В тексте использованы фрагметы реализации теста chameneos для SObjectizer v.4.4.

Hosted by uCoz