вторник, 27 июня 2017 г.

[prog.c++] Практически динамически-типизированное программирование

Давеча, занимаясь примером для демонстрации Asio-инфраструктуры для SObjectizer из нового проекта so_5_extra, написал фрагмент C++кода, в котором практически не фигурировали имена конкретных типов. Буквально возникло впечатление, что программирую на каком-то динамически-типизированном языке (правда, с излишне многословным синтаксисом). Кому интересно посмотреть немного C++14-того хардкора милости прошу под кат.

Итак, у меня был агент-менеджер, который должен был обрабатывать три сообщения, каждое из которых описывало результат ранее начатой операции: успешное завершение, неудачное завершение с конкретным кодом ошибки и истечение отведенного для операции тайм-аута. Хотя сообщения имели разный тип, но в них (не случайно) были общие поля, а уж обработка всех сообщений вообще оказалась более чем похожей. Если бы я реализовал эту обработку "в лоб", не боясь копипасты, то у меня получилось бы что-то вроде (уникальные фрагменты специально выделены полужирным курсивом):

void
on_resolve_successed( mhood_t<resolver_t::resolve_successed> cmd )
{
   const auto result_at = hires_clock::now();

   auto & item = m_data[ cmd->m_index ];
   item.m_timeout_timer.release();
   if( host_t::status_t::in_progress != item.m_status )
      return;

   --m_in_progress_now;
   const auto duration = ms( result_at - item.m_started_at );

   item.m_status = host_t::status_t::resolved;

   std::cout << item.m_name << " -> " << cmd->m_result
         << " (" << duration << "ms)"
         << std::endl;

   initiate_some_requests();
}

void
on_resolve_failed( mhood_t<resolver_t::resolve_failed> cmd )
{
   const auto result_at = hires_clock::now();

   auto & item = m_data[ cmd->m_index ];
   item.m_timeout_timer.release();
   if( host_t::status_t::in_progress != item.m_status )
      return;

   --m_in_progress_now;
   const auto duration = ms( result_at - item.m_started_at );

   item.m_status = host_t::status_t::resolving_failed;

   std::cout << item.m_name << " FAILURE: " << cmd->m_description
         << " (" << duration << "ms)"
         << std::endl;

   initiate_some_requests();
}

Ну и плюс еще одна такая же функция, которую я не стал приводить для экономии места.

Естественно, что мне такой объем копипасты сильно не понравился. Да и по ходу написания кода было впечатление, что набор действий при обработке ответа будет меняться (что и произошло), поэтому хотелось все эти действия локализовать в одном месте, дабы при модификации вносить изменения не в трех разных методах, а всего в одном.

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

void
on_resolve_successed( mhood_t<resolver_t::resolve_successed> cmd )
{
   handle_result(
      *cmd,
      [this]( const auto & result, auto & item, const auto duration ) {
         item.m_status = host_t::status_t::resolved;

         std::cout << item.m_name << " -> " << result.m_result
               << " (" << duration << "ms)"
               << std::endl;
      } );
}

void
on_resolve_failed( mhood_t<resolver_t::resolve_failed> cmd )
{
   handle_result(
      *cmd,
      [this]( const auto & result, auto & item, const auto duration ) {
         item.m_status = host_t::status_t::resolving_failed;

         std::cout << item.m_name << " FAILURE: " << result.m_description
               << " (" << duration << "ms)"
               << std::endl;
      } );
}

void
on_resolve_timeout( mhood_t<resolve_timeout> cmd )
{
   handle_result(
      *cmd,
      [this]( const auto & /*result*/auto & item, const auto duration ) {
         item.m_status = host_t::status_t::resolving_failed;

         std::cout << item.m_name << " FAILURE: TIMEOUT"
               << " (" << duration << "ms)"
               << std::endl;
      } );
}

templatetypename R, typename L >
void
handle_result( const R & msg, L && lambda )
{
   const auto result_at = hires_clock::now();

   auto & item = m_data[ msg.m_index ];
   item.m_timeout_timer.release();
   if( host_t::status_t::in_progress != item.m_status )
      return;

   --m_in_progress_now;

   lambda( msg, item, ms( result_at - item.m_started_at ) );

   initiate_some_requests();
}

И вот тут можно обратить внимание, насколько активно используется auto и как мало используется явных указаний имен типов. Фактически, названия конкретных типов присутствуют только в параметрах методов on_resolve_successed, on_resolve_failed и on_resolve_timeout. Все остальное -- это либо auto, либо параметры шаблона.

Можно, конечно, спорить на предмет того, разумно ли писать такой код для продакшена (все-таки это код из примера, а не фрагмент промышленного проекта). Но не могу не отметить двух важных вещей:

Первая состоит в том, что когда мы не привязываемся жестко к типам, мы получаем и большую гибкость, и уменьшаем себе количество забот. Для примера посмотрим на третий аргумент лябмда-функций из методов on_resolve_*. Он называется duration и он содержит количество миллисекунд, которые были потрачены на выполнение операции. Тут важно то, что я не знаю, какой именно тип будет у этого параметра. Ну в прямом смысле не знаю. Значение этого параметра вычисляется вот такой функцией:

static auto
ms( const hires_clock::duration duration )
{
   return std::chrono::duration_cast< std::chrono::milliseconds >(
         duration ).count();
}

Тут у меня получается значение типа std::chrono::milliseconds, из которого затем берется значение какого-то типа. Но какого? А вот без заглядывания в документацию и не скажешь. Да даже если и заглянешь, то, с большой долей вероятности, там будет какой-нибудь implementation specific целочисленный тип достаточной размерности. Какой точно не известно. И, что самое важное, это совершенно не важно. Современный C++ сам вывел всю цепочку нужных мне типов. Причем типобезопасно, без всяких усечений, потери знака и пр.

Более того, первоначально вспомогательная функция ms у меня вообще была шаблоном:

template<typename D> static auto
ms( const D duration )
{
   return std::chrono::duration_cast< std::chrono::milliseconds >(
         duration ).count();
}

Но мне показалось, что использовать такое в примерах -- это уже совсем хардкорно, и тип аргумента обозначил все-таки конкретным типом.

Вторая вещь, которую хотелось бы сказать, заключается в том, что на мой взгляд (подчеркиваю, на мой взгляд), именно такое почти что динамически-типизированное программирование на C++ с повсеместным использованием шаблонов есть и текущее конкурентное преимущество C++, и будущее C++, которое может помочь C++ пережить конкуренцию с тем же Rust-ом.

Пусть шаблоны C++ ведут к плохопонятным сообщениям об ошибках, пусть они замедляют компиляцию, пусть все это ведет к увеличению доли header-only библиотек (и связанных с этим проблем), пусть программирование на C++ становится похожим на программирование на Haskell-е... Пусть. Но именно шаблоны C++ сейчас позволяют C++ делать то, что не умеет ни Rust, ни какой-то другой из языков, нацеленных в ту же нишу. И если не пользоваться всем этим великолепием, то не проще ли сменить старичка C++ на модный и молодежный Rust? Там хотя бы Cargo есть, и нет Boost-а :)))

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