LINUX.ORG.RU

SObjectizer-5.7.0 с поддержкой send_case в select() и so5extra-1.4.0 под BSD-лицензией

 , , , ,


1

3

Вышли очередные версии библиотек SObjectizer и so5extra.

SObjectizer – это один из немногих все еще живых и развивающихся «акторных фреймворков» для C++ (еще есть QP/C++, CAF: C++ Actor Framework и совсем молодой еще проект rotor). Краткий обзор SObjectizer-а можно найти в этой презентации или в этой довольно старой уже статье. Хотелось бы подчеркнуть, что SObjectizer поддерживает не только модель акторов, но еще и такие модели как Publish-Subscribe и Communicating Sequential Processes. А so5extra – это набор дополнительных полезных прибамбасов для SObjectizer-а (например, реализованные на базе Asio диспетчер с пулом нитей и env_infrastructures, дополнительные типы message box-ов, средства для реализации синхронного взаимодействия и т.д.)

Если в двух словах, то:

  • SObjectizer-5.7 теперь позволяет использовать send_case в функции select(). Это делает SObjectizer-овский select() гораздо более похожим на select из Golang-а. Но это нововведение нарушило совместимость с предыдущей версией 5.6, т.к. теперь старая функция case_ стала называться receive_case;
  • в версии 5.7 устранен недочет в механизме доставки обернутых в конверты сообщений (т.е. enveloped messages) в случае использования transfer_to_state() и suppress() у агентов-получателей;
  • код so5extra теперь распространяется под BSD-3-CLAUSE лицензией, что позволяет бесплатно использовать so5extra при разработке закрытого программного обеспечения. Предыдущие версии распространялись под двойной лицензией (GNU Affero GPL v.3 и коммерческой);
  • в so5extra-1.4 реализованы mchain-ы фиксированной емкости для случаев, когда эта емкость известна на этапе компиляции.

Если же рассказывать более подробно, то основная фишка SObjectizer-5.7 – это возможность использования select() для отсылки исходящих сообщений (по аналогии с тем, как это происходит в Golang-е). Так что теперь можно делать вот такие вещи:

using namespace so_5;

struct Greetings {
   std::string msg_;
};

// Попытка отослать сообщения в соответствующие каналы,
// но все операции должны уложиться в 250ms.
select(from_all().handle_n(3).total_time(250ms),
   send_case(chAlice,
      message_holder_t<Greetings>::make("Hello, Alice!"),
      []{ std::cout << "message sent to chAlice" << std::endl; }),
   send_case(chBob,
      message_holder_t<Greetings>::make("Hello, Bob!"),
      []{ std::cout << "message sent to chBob" << std::endl; }),
   send_case(chEve,
      message_holder_t<Greeting>::make("Hello, Eve!"),
      []{ std::cout << "message sent to chEve" << std::endl; }));

В одном select() можно использовать и send_case() и receive_case() вместе. Например, вот SObjectizer-овская версия вычисления чисел Фибоначчи из в отдельном рабочем потоке (по мотивам из Golang’s tour):

using namespace std;
using namespace std::chrono_literals;
using namespace so_5;

struct quit {};

void fibonacci( mchain_t values_ch, mchain_t quit_ch )
{
   int x = 0, y = 1;
   mchain_select_result_t r;
   do
   {
      r = select(
         from_all().handle_n(1),
         // Отсылка сообщения типа 'int' со значением 'x' внутри.
         // Отсылка выполняется только когда values_ch готов для приема
         // новых исходящих сообщений.
         send_case( values_ch, message_holder_t<int>::make(x),
               [&x, &y] { // This block of code will be called after the send().
                  auto old_x = x;
                  x = y; y = old_x + y;
               } ),
         // Ожидание сообщения типа `quit` из канала quit_ch.
         receive_case( quit_ch, [](quit){} ) );
   }
   // Продолжаем операции пока что-то отсылается и ничего не прочитано.
   while( r.was_sent() && !r.was_handled() );
}

int main()
{
   wrapped_env_t sobj;

   thread fibonacci_thr;
   auto thr_joiner = auto_join( fibonacci_thr );

   // Канал для чисел Фибоначчи будет иметь ограниченный объем.
   auto values_ch = create_mchain( sobj, 1s, 1,
         mchain_props::memory_usage_t::preallocated,
         mchain_props::overflow_reaction_t::abort_app );

   auto quit_ch = create_mchain( sobj );
   auto ch_closer = auto_close_drop_content( values_ch, quit_ch );

   fibonacci_thr = thread{ fibonacci, values_ch, quit_ch };

   // Читаем первые 10 значений из values_ch.
   receive( from( values_ch ).handle_n( 10 ),
         // Отображаем каждое прочитанное значение.
         []( int v ) { cout << v << endl; } );

   send< quit >( quit_ch );
}

Полное описание нововведений версии 5.7.0 можно найти здесь.

Основное изменение в so5extra-1.4 – это смена лицензии на BSD-3-CLAUSE. Поэтому теперь все множество дополнений к SObjectizer-у из so5extra могут бесплатно использоваться в разработке закрытого коммерческого ПО.

Единственное нововведение в so5extra-1.4 – это реализация mchain для случая, когда максимальный объем mchain-а известен на этапе компиляции. Подобные mchain-ы зачастую используются в сценариях request-response, где ожидается всего одно ответное сообщение на запрос:

#include <so_5_extra/mchains/fixed_size.hpp>
#include <so_5/all.hpp>
...
using namespace so_5;

// Канал для получения ответного сообщения.
auto reply_ch = extra::mchains::fixed_size::create_mchain<1>(env,
   mchain_props::overflow_reaction_t::drop_newset);
// Отсылаем запрос.
send<SomeRequest>(target, ..., reply_ch, ...);
// Ждем и обрабатываем ответ.
receive(so_5::from(reply_ch).handle_n(1), [](const SomeReply & reply) { ... });

Надеюсь, что SObjectizer/so5extra кому-нибудь окажется полезен. Если есть вопросы, то спрашивайте, постараюсь ответить.

★★★★★

Ок, первый вопрос: почему код такой страшный?

hateyoufeel ★★★★★ ()
Ответ на: комментарий от hateyoufeel

первый вопрос: почему код такой страшный?

Очевидный ответ: потому, что его авторы не нашли способа сделать красивши.

Менее очевидный ответ: на вкус и цвет все фломастеры разные.

И если у вас нет цели потроллить, то покажите, что именно страшно, а я попробую объяснить, почему именно так.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

И если у вас нет цели потроллить, то покажите, что именно страшно, а я попробую объяснить, почему именно так.

Крайне маленький signal to noise ratio. Это любой код на плюсах такой или просто данный фреймворк? С бустом вроде столько мусора в коде не было.

hateyoufeel ★★★★★ ()
Ответ на: комментарий от hateyoufeel

Крайне маленький signal to noise ratio.

А можно еще более понятно для замшелых старпёров, которые не в ладах с английским?

Это любой код на плюсах

За любой говорить не буду, но доводилось встречать гораздо более страшный плюсовый код.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

А можно еще более понятно для замшелых старпёров, которые не в ладах с английским?

auto reply_ch = extra::mchains::fixed_size::create_mchain<1>(env,
   mchain_props::overflow_reaction_t::drop_newset);

Ну вот на эту строчку посмотри, например. Неужели нельзя уменьшить количество вложенных неймспейсов?

hateyoufeel ★★★★★ ()
Ответ на: комментарий от hateyoufeel

Так в C++ каждый сам себе может давать псевдонимы как хочет.

Типа такого:

namespace fixed_mboxes = so_5::extra::mchains::fixed_size;
...
auto reply_ch = fixed_mboxes::create_mchain<1>(...);

Можно вообще делать using namespace и будет еще короче.

eao197 ★★★★★ ()
Ответ на: комментарий от hateyoufeel

overflow_reaction_t

Вот эта привычка из Си — прилеплять суффикс, она в Си++ выглядит странно. Ведь уже засунули в неймспейс. Казалось бы, называй нормально уже, но нет.

i-rinat ★★★★★ ()
Ответ на: комментарий от i-rinat

Очень тяжело разбираться в коде, который написан в стиле boost или stdlib, в котором имена типов и имена переменных ничем не отличаются. Поэтому и используем суффикс _t. Пробовали отказаться несколько раз, но потом все равно к нему возвращались, т.к. код оказывается (для нас) сильно читабельнее.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

so_5::extra

Судя по пресс-релизам, вы там регулярно ломаете совместимость, но вещи типа so_5 и extra остаются такими же, как были. Какой смысл в сокращении основного пространства имён до so_5? Если пользователям предлагается использовать псевдонимы для сокращения кода, то можно и подлиннее имя взять, с полным названием. Если предполагается, что все будут использовать неймспейс как есть, нет смысла в _5, достаточно оставить so.

Теперь про extra. Ну я понимаю, что это extra. Но зачем пользователям каждый раз это в лицо совать? Всё равно эту extra делают те же разработчики, что и основную часть. Нет смысла отделяться в отдельный неймспейс, потому что добавление чего-то в основной части всё равно согласовано с extra. Слово extra не нужно, можно использовать основной неймспейс.

i-rinat ★★★★★ ()
Ответ на: комментарий от eao197

Если _t убрать в mchain_props::overflow_reaction_t::drop_newset, появятся какие-то сложности с чтением? Я правда не понимаю.

i-rinat ★★★★★ ()
Ответ на: комментарий от i-rinat

Вот эта привычка из Си — прилеплять суффикс, она в Си++ выглядит странно.

nullptr_t, void_t :)

hateyoufeel ★★★★★ ()
Ответ на: комментарий от hateyoufeel

Я же говорю о бессмысленности _t суффиксов там, где они не нужны. А в твоём примере они как раз нужны. У меня нет маниакальной одержимости убирать _t отовсюду, где только можно.

i-rinat ★★★★★ ()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от i-rinat

Судя по пресс-релизам, вы там регулярно ломаете совместимость

Да, на постоянной основе. С октября 2014-го, когда была выпущена v.5.5.0, сломали совместимость целых два раза. Первый раз весной 2019-го, когда выпустили 5.6.0 после двух с половиной десятков релизов в рамках ветки 5.5. И вот сейчас. После того, как в ветке 5.6 было еще пара неломающих ничего релизов.

Если предполагается, что все будут использовать неймспейс как есть, нет смысла в _5, достаточно оставить so.

В дикой природе есть еще кодовые базы, в которых SObjectizer-5 используется совместно с SObjectizer-4. Так что so_5 и so_4 все еще имеют смысл.

Всё равно эту extra делают те же разработчики, что и основную часть.

Ранее это были два продукта с разными условиями распространения.

Если теперь все снести из extra в SObjectizer, то придется рефакторить код, который уже so5extra использует. Непонятно кому из тех, кто пользуется SO-5/so5extra нужен этот труд.

Кроме того, SO-5 не имеет внешних зависимостей. Тогда как so5extra их уже имеет и, вероятно, в будущем будет иметь еще больше.

eao197 ★★★★★ ()
Ответ на: комментарий от i-rinat

Если _t убрать в mchain_props::overflow_reaction_t::drop_newset, появятся какие-то сложности с чтением? Я правда не понимаю.

Сложности со чтением появятся когда в коде будет использоваться mchain_props::overflow_reaction_t как тип. Так сразу видно, что это тип. А без суффика – хз что. Константа, функция, тип, пространство имен…

eao197 ★★★★★ ()

Не рассматриваете возможность переноса кодовой базы на Rust?

Действительно, код может оказаться по-проще, и да и мне кажется, что ниша пока не занята. В Rust немного повернуты на опенсорсе, а у вас - оно самое, если я правильно понял.

dave ★★★★★ ()

В Rust немного повернуты на опенсорсе

Здесь я имею в виду, что нет возможности скрыть код библиотеки. Видимо, задумано так.

dave ★★★★★ ()
Ответ на: комментарий от dave

Не рассматриваете возможность переноса кодовой базы на Rust?

За собственный счет – нет. Если кто-то согласится спонсировать такое портирование, то вполне возможно. Хоть на Rust, хоть на C#, хоть на Kotlin, хоть на D.

Впрочем, на эту тему я свои мысли уже излагал: https://eao197.blogspot.com/2018/10/progthoughts-sobjectizer-6.html Тогда еще не было понятно, что делать после SO-5.5.

В С++ мы все еще остается потому, что C++ нас кормит. Поэтому мы и сами знаем, что нужно в C++, и результаты своих трудов и сами применим можем, и другим можем показать как. Поэтому затраты на разработку SO-5/so5extra для C++ хоть как-то отбиваются. С Rust-ом ситуация совсем другая, т.к. мы на нем ничего не делаем. И вряд ли сейчас за Rust платят, тем более маленьким компаниям.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

И я не знаю, какая модель монетизации у Rust, если это не криптобиржа или не in-house девелопмент. Да и будущее Rust для меня в тумане, хотя сам язык мне очень понравился.

dave ★★★★★ ()
Ответ на: комментарий от dave

У меня лично нет сомнения в перспективах Rust-а и его жизнеспособность опасений не вызывает. Но, как мне представляется, сейчас на Rust-е ведется, в основном, in-house разработка. Т.е. нужны Rust-о-погромисты в штат. Ну или это бесплатный OpenSource для души.

Время, когда на сторону раздают заказы на разработку/поддержку Rust проектов пока, вроде бы, еще не наступило.

Так что предлагаю закрыть это направление как офтопик.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Очень тяжело разбираться в коде, который написан в стиле boost или stdlib, в котором имена типов и имена переменных ничем не отличаются.

Это как раз признак того, что в вашем коде «крайне маленький signal to noise ratio».

rupert ★★★★ ()
Ответ на: комментарий от hateyoufeel

nullptr_t, void_t

Очевидно, в данном случае nullptr и void уже заняты.

rupert ★★★★ ()
Ответ на: комментарий от i-rinat

А смысл их оставлять тогда? Оставили бы в своей сишке, но нет, притащили и сюда.

RazrFalcon ★★★★★ ()
Ответ на: комментарий от rupert

Это как раз признак того, что в вашем коде «крайне маленький signal to noise ratio».

Пока что совсем малое отношение сигнала к шуму наблюдается в комментариях.

В C++ нет общепринятого стиля наименования. Куча библиотек в PascalCase, куча в camelCase, куча в snake_case, есть даже в Ace_Case. Суффиксы _t для типов используем не только мы. Можно сколько угодно доказывать что у нас самая говняная нотация (на самом деле нет), но проект развивается с таким соглашением об именовании уже почти 18 лет.

Вы серьезно думаете, что умничание в комментариях на LOR-е заставит нас перейти на какую-то другую нотацию?

eao197 ★★★★★ ()

Почему это не в новостях?

bhfq ★★★★★ ()
Ответ на: комментарий от bhfq

Тут мало чего-то значимого по сравнению с релизом 5.6.0, о котором новость была.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Да мне (и уверен, остальным здесь тоже) глубоко положить на вашу нотацию. Проблема у вас не с нотацией. А с тем, что код с использованием вашей библиотеки читать очень сложно. Не видно с первого взгляда, что к чему. Нужно вчитываться в каждую строчку, чтобы хоть что-то понять. Отсюда и замечания, что в вашем коде отношение сигнала к шуму мало как никогда. Ваша нотация - ничто иное как реакция на сложность кода.

rupert ★★★★ ()
Ответ на: комментарий от rupert

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

Мне интересно, а вот те, кто так говорят, он насколько знакомы с нашей библиотекой? Базовую документацию хотя бы прочитали? Имеете представление о назначении и возможностях?

А то ведь если человеку, который никогда Asio или Crypto++ не видел, дать посмотреть на примеры кода с Asio, где asio::ip::tcp::endpoint или asio::ip::tcp::resolver::iterator в перемешку с async_read и asio::error_code… Да даже если человеку, не знакомому с генераторами псевдослучайных чисел из C++11 показать кусочек примера с cppreference, типа такого:

int main()
{
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(1, 6);
 
    for (int n=0; n<10; ++n)
        std::cout << dis(gen) << ' ';
    std::cout << '\n';
}

Многое ему будет понятно из всех этих random_device, mt19937 и uniform_int_distribution?

При этом сеть и псевдослучайные числа – это хотя бы более-менее знакомые для многих вещи.

Тут же, если говорить про SObjectizer-овские mchain-ы, вряд ли отметившиеся в комментариях понимают что это такое и какой функциональностью mchain-ы обладают.

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

   auto values_ch = create_mchain( sobj, 1s, 1,
         mchain_props::memory_usage_t::preallocated,
         mchain_props::overflow_reaction_t::abort_app );

   auto quit_ch = create_mchain( sobj );

И что более сложная конструкция, при наличии тут же гораздо более простой, приведена не просто так.

eao197 ★★★★★ ()
Последнее исправление: eao197 (всего исправлений: 1)
Ответ на: комментарий от rupert

В продолжение к предыдущему комментарию. Взять хотя бы текст функции main() из стартового сообщения:

int main()
{
   wrapped_env_t sobj;

   thread fibonacci_thr;
   auto thr_joiner = auto_join( fibonacci_thr );

   // Канал для чисел Фибоначчи будет иметь ограниченный объем.
   auto values_ch = create_mchain( sobj, 1s, 1,
         mchain_props::memory_usage_t::preallocated,
         mchain_props::overflow_reaction_t::abort_app );

   auto quit_ch = create_mchain( sobj );
   auto ch_closer = auto_close_drop_content( values_ch, quit_ch );

   fibonacci_thr = thread{ fibonacci, values_ch, quit_ch };

   // Читаем первые 10 значений из values_ch.
   receive( from( values_ch ).handle_n( 10 ),
         // Отображаем каждое прочитанное значение.
         []( int v ) { cout << v << endl; } );

   send< quit >( quit_ch );
}

Она же не просто так настолько сложная. Не зря в ней делаются вызовы auto_join и auto_close_drop_content. Не зря fibonacci_thr сперва объявляется, а лишь потом для нее создается реальный рабочий поток.

За всем этим стоят объективные причины, вызванные как особенностью языка, так и особенностями его стандартной библиотеки. И что в SObjectizer-е часть функциональности нужна, чтобы справляться с этими особенностями.

Поэтому и примеры приводятся такие, в которых эти особенности учтены.

Можно было бы и компактнее написать, с меньшим колличеством букв. Но тогда бы пример был бы не exception-safe. Т.е. показывал бы как отстрелить себе конечности, а не защититься от этого.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Можно было бы и компактнее написать, с меньшим колличеством букв. Но тогда бы пример был бы не exception-safe. Т.е. показывал бы как отстрелить себе конечности, а не защититься от этого.

От этих exceptions столько геморроя! Как думаешь, стоило ли их вводить в язык? Многие скажут, что у истории сослагательного наклонения не бывает, но все же, просто из любопытства.

Или я снова ухожу/увожу от темы :)

dave ★★★★★ ()
Последнее исправление: dave (всего исправлений: 1)
Ответ на: комментарий от dave

От этих exceptions столько геморроя!

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

FILE * fin = fopen(...);
if(fin) {
  FILE * fout = fopen(...);
  if(!fout) return -1;
  ...
  fclose(fin);
}
else return -1;

Как думаешь, стоило ли их вводить в язык?

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

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

Возможно, если бы что-то вроде std::error_code появилось в конце 1980-х и в качестве исключения можно было бы бросать только экземпляры std::error_code, то исключения в C++ были бы сильно дешевле.

Но, с другой стороны, не было бы простой и удобной возможности распространять вместе с исключением подробной информации об ошибке…

Так что, как по мне, основная проблема с нынешними исключениями – это не их наличие, а их высокая стоимость. В смысле они дороги, когда их бросают.

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Кстати говоря, на счет сложности фреймворка.

Вот две статьи, которые ретроспективно показывают, как SObjectizer-5 развивался на протяжении не такого уж большого отрезка времени. Сколько всего он вобрал, что оказалось сделано не так, и было переделано, а что затем вообще было выброшено.

Четыре года развития SObjectizer-5.5. Как SObjectizer изменился за это время?

SObjectizer-5.6.0: режем по живому, чтобы расти дальше

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

eao197 ★★★★★ ()
Ответ на: комментарий от eao197

Что-то все сложные схемы получаются. Из-за того, что задача сложная?

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

Просто эти исключения столько проблем создают в С++. С другой стороны, действительно крутой механизм. Они появились еще в Ada 83, может быть, еще где-то раньше (в лиспе?). Тогда они широко раскручивались. Поэтому неудивительно, что исключения были добавлены в C++, потому что костяк C++ формировался как раз в те годы. Скажем, это фаза «тезиса», пользуясь терминологией диалектики.

Как понимаю, сейчас фаза «антитезиса», когда стало модно в современных языках использовать коды возврата или ошибки возврата как в Rust.

Интересно было бы узнать, что будет в фазу «синтеза» лет через 10 или 20, или даже 30.

dave ★★★★★ ()
Ответ на: комментарий от eao197

основная проблема с нынешними исключениями – это не их наличие, а их высокая стоимость. В смысле они дороги, когда их бросают.

Как оказалось, они еще не бесплатны даже если их не выкидывают. Но по мне коды возврата и исключения это два разных способа решения задач со своими достоинствами и недостатками и оба нужны.

yetanother ()
Последнее исправление: yetanother (всего исправлений: 1)
Ответ на: комментарий от yetanother

Но по мне коды возврата и исключения это два разных способа решения задач со своими достоинствами и недостатками и оба нужны.

Именно так.

eao197 ★★★★★ ()
Ответ на: комментарий от dave

Что-то все сложные схемы получаются. Из-за того, что задача сложная?

Мне не кажется, что она сложная. Скорее проблема в том что пользователь оказывается перед выбором: использовать ли ему исключения или коды возврата? А выбор – это всегда трудно.

Интересно было бы узнать, что будет в фазу «синтеза» лет через 10 или 20, или даже 30.

Интересно. Но работать нужно с чем-то здесь и сейчас. А точнее – еще вчера.

Когда создавался SO-4, предшественник SO-5, от исключений было решено оказаться. Т.к. до этого я «наелся» исключений в другом проекте, где они использовались сверх меры.

Но время показало, что без исключений плохо. Слишком много ошибок, которые можно было по недосмотру проигнорировать, было проигнорировано.

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

И вроде как не первый год уже полет нормальный. Может быть потому, что в нормальных условиях исключения просто так не бросаются.

eao197 ★★★★★ ()
Ответ на: комментарий от dave

У меня были времена, когда я недолюбливал исключения, но опыт привёл меня к тому, что они часто меньшее зло. На кодах возврата неудобно протаскивать информацию о состоянии/причинах ошибки, из-за чего распухает код и усложняется API. А смотря, как реализованы и используются коды возвратов в том же го, я хочу разрыдаться, т.к. код замусорен ровно наполовину, коллеги даже показывали плагины для IDE, которые прячут этот мусор.

filosofia ()
Ответ на: комментарий от filosofia

На счет IDEA прикольно. Они, кстати, и в Java кое-какие шаблоны прячут. Получается, лишний синтаксический шум.

dave ★★★★★ ()
Ограничение на отправку комментариев: только для зарегистрированных пользователей