Pull to refresh

Comments 61

Что-то в пункте «стало» не хватает типа CoroTask и PromiseType. Что-то с такой реализацией не очень похоже на удобство.
CoroTask и PromiseType пишутся один раз, используются по всему проекту во всех функциях. Можно конечно вынести их в пункт «стало», да и WorkQueue заодно… или оставить всё как есть и сравнивать именно тела функций :)

Хм… Интересно, но я не очень понял как это соотносится, например, с корутинами Котлина, где в library space выставляется примитивы suspend_coroutine/resume, позваляющие делать с корутиной что хочешь (выложить в фоновый пул, ждать там завершения, генераторы...). Кажется, здесь что-то похожее, но требуется явно писать сo_yield — правда же, что там можно как хочешь сохранять корутину и где угодно продолжить?

С Котлин я особо не знаком, но кажется что весьма похоже: хотите остановить корутину — пишите co_await (для генераторов будет удобнее co_yield), хотите корутину возобновить в каком-то другом потоке/функции — вызывайте operator() на нужном вам coroutine_handle.

Возобновлять можно в любом месте, приостанавливать можно почти что в любом (вы не сможете написать co_await/co_yield в конструкторе или деструкторе)

Угу, действительно похоже. А если захочется абортнуть корутину, можно ей Exception запихать при возобновлении?

Да, но придётся самому ручками прописать логику. В следующей статье постараюсь описать, как это делается.

Ждём, вы прекрасно пишете! На самом деле, если сможете собрать пример, похожий на генераторы в Питоне, ещё и с abort, то он покроет все примеры.

UFO just landed and posted this here
Конкретные цифры я не приведу но дела обстоят приблизительно так:
* новый поток отъедает где-то 2 MB оперативной памяти под стек. Корутина отъедает ровно столько памяти, чтобы можно было сохранить все локальные переменные функции. В зависимости от функции, это на 3-4 порядка меньше.
* переключение потоков — это тяжёлая операция связанная с переключением контекстов ОС. 1000 активных потоков выест ваш процессор одними только переключениями контекстов. Возобновление корутины — это приблизительно так же тяжело, как и вызов функции по указателю. Тоесть опять на порядки легче.
В примерах с корутинами мне обычно не понятно, как возобновить работу корутины.
С приостановить то все боле-менее ясно, сохраняем стек и все (ну может есть какие-то внутренние сложности, не важно)
А вот как возобновить его работу?
То есть, ждем мы какого-то события по сети (например, нам в сокет что-то написали). При этом событии корутина пробудится сама, или нужно время от времени ее опрашивать в while (true)?

Почему-то про всех статьях про корутины от меня этот момент ускользает, или у меня не хватает терпения дочитать до конца. Было бы хорошо, если бы это кто-то объяснил на пальцах

Например, в этом примере. Хотелось бы увидеть код, который вызывает функцию CoroToDealWith()
В данном случае примерно понял. Корутина возобновится, когда поток возьмет задание из очереди и попытается его выполнить.
Но остался вопрос про «автоматическое» возобновление, возможно ли такое
Давайте для примера возьмём сеть. Большинство библиотек для асинхронной работы с сетью построены через callback. Когда пакет получен — вызывается callback. Вот в качестве callback и надо передавать coroutine_handle.

Вот так например выглядит эхо сервер с использованием ASIO и Coroutines TS. Если упрощённо, то выражение `co_await socket.async_read_some` получает coroutine_handle на текущую функцию и регистрирует этот coroutine_handle как callback при получении пакета. Если пакет не получен — ничего не просыпается и не опрашивается. Когда пакет получен, вызывается coroutine_handle и функция echo возобновляет свою работу.
Ага, понятно. То есть, где-то на низком уровне код все равно должен поддерживать корутины хотябы через обычные коллбэки. То есть невозможно прямо весь код снизу доверху написать на корутинах?

То есть, в обычном однопоточном коде я могу написать что-то вроде
while (true) {
int n = read(fileFd, buffer);
}
А в коде с корутинами, даже если я напишу
while (true) {
int n = co_await read(fileFd, buffer);
}
и даже если read будет поддерживать коллбэки, то получается, что где-то все равно должен быть отдельный поток, который бы проверял статус готовности file descriptor-а?
Это не обязательно отдельный поток — можно реализовать через ОС специфичные методы для асинхронной работы с сокетами/дескрипторами и обойтись одним потоком. Будет ожидание одного из сетевых событий на множестве сокетов, а при его наступлении — будет пробуждаться нужная корутина и запускаться в том же потоке.
Ага, ну примерно про это у меня и был первый вопрос. То есть, корутина каким-то чудом пробуждается сама (на самом деле, ее будет дергать ядро, но это детали). Вот и интересно, насколько такое «чудо» глубоко проникнет по различным функциям, и можно ли будет это «чудо» организовать самому (например я захочу сделать что-то вроде сокета, чтобы при изменении его в другом потоке корутина пробуждалась)

Просто мне интересно, если например в произвольном коде в (почти) произвольных местах понарасставить co_await, то будет ли это все работать корректно и асинхронно?
Вроде нечто подобное делают здесь (https://habr.com/company/jugru/blog/422519/), если я правильно это понимаю
Просто понарасставлять co_await компилятор не позволит. Если возвращаемое значение функции не содержит правильный promise_type — будут ошибки компиляции.

Если же есть promise_type и они правильно написаны под то, как вы используете корутины — то да, всё заработает.
Да, имеется в виду, что promise_type проставлен будет. В этом и вопрос, собираются ли разработчики компиляторов добавлять нужный promise_type в существующие функции, типа read, poll, mutex и т.д. То есть, о чем выше говорили, что ядро + компилятор обеспечивают «бесшовное» внедрение корутин.

То есть, если я правильно понимаю статью по ссылке выше, в яве как раз пилят файберы, которые просто позволяют сделать обычный синхронный код «типа corutines». Вот будет ли что-то подобное в C++?
Да, идёт работа над Networking TS. В нём предусмотрена возможность в дальнейшем интегрироваться с корутинами. + в стандартную библиотеку должны завести набор примитивов, упрощающих разработку проектов с использованием сопрограмм.

Но всё это будет после C++20. А до тех пор, придётся либо писать самому, либо использовать наработки из сторонних библиотек (например Boost.ASIO).
а не получится как со строками — на каждый крупный проект свой велосипед?
Неужели я один такой в шоке от того что за последние 15 лет сделали с С++… Интересно, что думает Б.Страуструп, Г.Шилдт, П.Нортон об этих «стандартах»? С момента появления STL код растёт в объёме на порядки, производительность падает, классы и ООП лепят там где надо и не надо, память динамически всюду, про фрагментацию не думают, лишь бы сказать, что код написан с паттернами "***". (*** — то что модно в этом году).
Мне кажется, что Вы путаете развитие языка С++ и то, как на нем разрабатываются приложения. Никто не заставит Вас использовать С++17, если Вы того не захотите, ведь обратная совместимость никуда не девается (в большинстве случаев). Пишите в процедурном стиле, делайте свои аллокаторы с непрерывной памятью и без паттернов. Принцип «не платишь за то, что не используешь» никуда не делся.
Вопрос двоякий:
* «код растёт в объёме на порядки» — да, бинарники становятся всё больше и больше. Это связано не только с шаблонами, но и с исключениями и с RTTI (и с объёмом написанного кода/функционалом приложения). Многие проекты успешно борются с размером бинарников — LLVM например использует type erased вещи во многих местах (вот пример контейнера, который специально имеет нешаблонный базовый класс, дабы передавать именно его в функции, не раздувая размер бинарников).
* «производительность падает» — тут не согласен. Всё зависит от того, как код написать. Для хорошо написанного кода производительность от стандарта к стандарту только растёт.
* «память динамически всюду» — зависит от проекта. В стандартной библиотеке тоже есть места, где происходят динамические аллокации… Это конечно зло, но кажется что зло неизбежное.
UFO just landed and posted this here
Особенность С++ проектов в том, что там код больше читают, чем пишут. А вообще почему Яндексу не использовать какой-нибудь аналог scheme для построения коррутин? Неужели так удобнее…
UFO just landed and posted this here
> Особенность С++ проектов в том, что там код больше читают, чем пишут.

Это особенность не C++, а любого серьезного проекта, вне зависимости от языка программирования.
современные плюсы от этой ерунды как-то отходят даже


А куда отходят, и что вместо них?
UFO just landed and posted this here
А у вас есть пара примеров проектов написанных в подобном стиле с исходниками в открытом доступе? Очень интересно посмотреть!
UFO just landed and posted this here
Интересно, что думает Б.Страуструп, Г.Шилдт, П.Нортон об этих «стандартах»?

не знаю за остальных, но посмотрев видеозаписи выступлений Страуструпа кажется, что он настроен оптимистично. Но вам, разумеется, виднее
а всё-таки, что с исключениями? Улетят ли они по умолчанию в вызов coroutine_handle, как это сделано в std::async?
Нет. Все выкинутые из корутины исключения будут пойманы, а в блоке catch(...) будет вызвана функция promise_type::unhandled_exception. В ней вы можете сохранить исключение в promise_type.

Могу расписать подробности в след. статье. Хотите?
Не очень понимаю, почему в каждом туториале по корутинам начинаются какие-то сложности. Это ведь всего лишь функция с сохраняемым стеком, она должна быть почти такой же простой, как и обычный вызов функции?
Проблемы связаны с тем, что туториалы не совсем базовые. Согласитесь, если в примере есть многопоточность и неявные состояния — то тут сложностей не избежать.

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

Тот promise_type, что описан в статье, должен быть в стандартной библиотеке. Тогда всё решение задачи сократится до одного параграфа.
Обычно с удовольствием читаю этот блог по C++, но данная статья исключение. Причём отторжение вызывает сама концепция статьи: возьмём абсолютно невозможный в приличном проекте код и успешно победим его с помощью новых технологий. Подобная сомнительная демонстрация совсем не убеждает в преимуществах новых подходов, т.к. если показанное является главным примером применения, то значит оно просто не нужно в реальных проектах.
ИМХО, конкретно у данной статьи есть два фактора, которые сказываются на простоте ее восприятия.

1. Мало кто (и я, например, точно не из их числа) разбирался с TS-ом по короутинам и представляет себе, что именно делает компилятор, когда встречает co_await/co_yield/co_return, какой код генерируется и как это затем работает. А с таким пробелом в знаниях и понимании работы предложенных в TS-е короутин воспринимать приведенный в статье код очень тяжело.

2. Сам подход, когда мы пишем «типа линейный» код, разные куски которого затем должны работать на разных рабочих контекстах и переключение этих контекстов происходит каким-то непривычным «магическим» образом, так же с непривычки вызывает… эээ… если не отторжение, то сложности с пониманием. Мне, например, привычнее было бы работать с первоначальным кодом, где лямбды с действиями явным образом распихивались по разным очередям задач. Более четко видно что и куда уходит. Но это, возможно, всего лишь дело привычки.

Так вот, если сложить эти два фактора, то и получается реакция, вроде вашей. Но когда материалов по принципам работы stackless coroutines из будущего C++ станет больше и все больше и больше сиплюсплюсников с этими вещам окажутся знакомы, тогда данная тема может перестать казаться такой сложной и сомнительной.
Мне, например, привычнее было бы работать с первоначальным кодом, где лямбды с действиями явным образом распихивались по разным очередям задач. Более четко видно что и куда уходит. Но это, возможно, всего лишь дело привычки.

С корутинами бизнес-логику приложения и логику планировщика можно полностью изолировать друг от друга. При этом первое описывается в императивном стиле, а не раскидано по разным лямбдам
Давайте я еще раз поясню свою мысль. Когда у меня есть код вида:
void foo(some_task params) {
  auto data = allocate_and_prepare_task(params);
  data.save_to(params.data_file());
  show_data(data.presentation_view());
}

И в этом коде для выполнения каждой операции происходит перевод foo() с одного контекста на другой, т.е.:
void foo(some_task params) {
  // Здесь неявное переключение на нить для "тяжелых" вычислений.
  auto data = allocate_and_prepare_task(params);
  // Здесь неявное переключение на IO-нить.
  data.save_to(params.data_file());
  // Здесь неявное переключение на GUI-нить.
  show_data(data.presentation_view());
}

то лично мне (повторю специально: лично мне) не хочется иметь дело с таким кодом. Я бы предпочел что-то более явное:
void foo(some_task params) {
  auto data = perform_on(cpu_thread, [&]{ return allocate_and_prepare_task(params); });
  perform_on(io_thread, [&]{ data.save_to(params.data_file()); });
  perform_on(gui_thread, [&]{ show_data(data.presentation_view()); });
}

Это мои личные заморочки и тот факт, что «С корутинами бизнес-логику приложения и логику планировщика можно полностью изолировать друг от друга» (чтобы под этой умной фразой не подразумевалось) в моих личных предпочтениях ничего не меняет.

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

Я так полагаю, аналогичный код на корутинах будет выглядеть так:
coro_type foo(some_task params) {
    co_yield cpu_thread;
    auto data = allocate_and_prepare_task(params);
    co_yield io_thread;
    data.save_to(params.data_file());
    co_yield gui_thread;
    show_data(data.presentation_view());
}

Думаю, такой аналог читается достаточно просто независимо от предпочтений
Думаю, такой аналог читается достаточно просто независимо от предпочтений
Я не знаком с coroutine TS, поэтому у меня нет понимания следующих вещей:

1. Можно ли явным образом ограничивать область действия co_yield-а? Т.е. писать что-то вроде:
co_yield some_context {
  action_one();
  action_two();
  ...
}
Поскольку меня смущает то, что co_yield меняет контекст для всего последующего кода. Это может вызывать сложности, если кто-то напишет что-то вроде:
if(some_condition)
  co_yield first_context;
else if(another_condition) {
  co_yield second_context;
  some_action();
}
else
  another_action();
main_action(); // ???

И вот пойми потом, на каком контексте будет работать main_action, намеренно ли это было сделано или стало результатом ошибки.

2. Как можно передавать дополнительные аргументы в co_yield. С некой условной функцией perform_on можно без проблем сделать так:
perform_on(cpu_thread, priority{low}, deadline_timer{20s}, [&]{...});

В случае с co_yield можно ли будет делать, например, вот так:
co_yield requirements(cpu_thread, priority{low}, deadline_timer{20s});
... 
1. Можно ли явным образом ограничивать область действия co_yield-а? Т.е. писать что-то вроде:

co_yield же не вводит никакую «область действия». co_yield arg; по сути просто вызывает yield_value(arg) для возвращаемой таски. В примере yield_value указывает в какой поток передать корутину перед тем как она заснет. А реально контекст (поток выполнения) переключает PromiseType
В случае с co_yield можно ли будет делать, например, вот так:

requirements(...) может возвращать конфиг в виде структуры/тапла, который примет yield_value. Выглядит это как-то так:
пример
// Конфиг, который будет передаваться в co_yield
struct YieldConfig {
    WorkQueue &wq;
    int a;
    int b;
};

// Перегрузка yield_value, принимающая конфиг
auto yield_value(YieldConfig c) {
    std::clog << "yield config: a=" << c.a << ", b=" << c.b << std::endl;
    yield_value(c.wq);
}

// Вызов yield_value(YieldConfig) + designated initializers
co_yield {.wq = networkQueue, .b = 5}; 

Модифицированный пример из статьи

вообще конечно лучше бы вызов yield_value из co_yield происходил через std::invoke, чтобы можно было делать так:
auto yield_value(auto&& ..args);
// ...
co_yield tuple(args...);

Но это уже к комитету

Это может вызывать сложности, если кто-то напишет что-то вроде:

люди и без корутин иногда пишут сложный/неоптимальный/некорректный код.
Можно ли явным образом ограничивать область действия co_yield-а? [...] Поскольку меня смущает то, что co_yield меняет контекст для всего последующего кода.

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


Не вижу смысла в подобных областях действия.

Не вижу смысла в подобных областях действия.

Замечательно. Только вот это никоим образом не говорит о том, что смысла не видят другие люди. Пардон май френч.

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

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


Я спрашивал про ограничение контекста для того, чтобы в коде было явно видно — вот этот набор операций на этом рабочем контексте, а вот этот — на вот этом. Что-то вроде:


co_yield calculation_ctx {
  data = do_some_complex_calculations();
}
co_yield db_ctx {
  store_calculated_results_to_db(data);
}
co_yield gui_ctx {
  show_data_calculation_status(data);
}

Естественно, в каждой секции может быть больше одной операции.

Но этот вариант невозможен, и как раз объяснил почему. Попробуйте перечитать мой комментарий с начала, а не с конца.

А вы представьте, что ваши комментарии читаются, но ваши объяснения, при этом, оказываются недостаточными и бесполезными. Интересующие меня ответы с достаточно подробными и внятными объяснениями (в отличии от) были получены задолго до того, как вы решили донести до меня свое веское мнение.


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

Сопрограммы точно не вызывают у меня никакой сложности или отторжения — если что, я для регистрации на Хабре (лет 5 назад) написал вот habr.com/post/185706 такую статейку.

А не нравится мне в данной статье полное отсутствие внятной аргументации преимуществ от применения сопрограмм. И да, это на самом деле не такой тривиальный вопрос, потому как главным применением сопрограмм в данном контексте является линеаризация асинхронного кода, который сам по себе далеко не всегда полезен. Указанную в начале статьи задачку можно было решить множеством разных способов. Например с помощью древних банальных системные потоки с блокировками (и кстати с ними код тоже получается линейным, только автоматически). Или же можно было взять более продвинутый инструмент — одну из многих готовых библиотек, реализующих модель акторов (прямо идеально ложится на обсуждаемую задачу). И всё это отлично работает (с более высоким быстродействием и вполне лаконичным кодом) прямо сейчас, без необходимости использования не вошедших в стандарт языка расширений.

Безусловно есть узкие области, где применение асинхронного кода, «выпрямленного» сопрограммами крайне полезно (примеры можно увидеть скажем в Boost.Asio). А так же есть специфические очень полезные применения сопрограмм вообще без многопоточности (например вместе с Ranges). Однако про это всё в статье нет ни слова.

Т.е. для меня данная статья выглядит приблизительно так:
Давайте рассмотрим постройку дачи из стекла. В её процессе рабочие частенько бьют стекло и много матерятся. Однако теперь у нас появился новейший инструмент (липучки для стекла!), с которым постройка дачи из стекла будет сопровождаться гораздо меньшим матом. При этом никаких упоминаний о том, что любой нормальный архитектор будет строить дачу из дерева/бетона/ещё чего, а конструкции из стекла применит только для очень особенных зданий, в статье конечно же нет…
А какие преимущества от применения сопрограмм являются для вас основными?
На данный момент я на практике встречал три области, в которых ощущалась потребность в сопрограммах:

1. Линеаризация асинхронного кода. Актуально для многопоточного кода с тысячами одновременных задач (когда системные потоки становятся неэффективными). Хорошим примером является реализация нагруженного сервера с помощью Boost.Asio (кстати такой пример есть прямо в самой документации Asio).

2. Написание различных генераторов. Это становится особо удобно и актуально с приходом в язык диапазонов (Ranges). Хорошие примеры можно увидеть здесь youtu.be/LNXkPh3Z418?t=1992.

3. Переписывание обычного кода в парадигму реактивного программирования (причём с помощью Stackful сопрограмм это можно делать даже без модификации кода). Например можно взять парсер из Boost.Spirit и в пару строк сделать его реактивным (можно будет кормить его данными по байтам). Кстати, по этой области тоже есть хорошие примеры в видео из предыдущего пункта.

Да, так вот код, использованный в данной статье в качестве примера, явно не относится ни к одной из этих областей. А причиной его изначальной лапшевидности является вовсе не отсутствие сопрограмм в языке, а криворукость архитектора (это можно было записать красиво и без всяких сопрограмм), если конечно же это реальный код, в чём я сильно сомневаюсь (подозреваю что это всего лишь искусственный пример, выдуманный ради статьи). И соответственно очень трудно продемонстрировать преимущества сопрограмм, на примере задачи, в котором они банально не нужны.
Как бы вы записали этот код красиво и без сопрограмм?
Ну например вот так:
код
void FuncToDealWith() {
	InCurrentThread();
	Send(writer, Op1{});
}

class Writer: public Actor{
//...
	OnOp1(const Op1&){
		InWriterThread1();
		if (NeedNetwork()) Send(network, Op1{});
		else Send(writer, Finally{});
	}
	OnFinally(const Finally&){
		InWriterThread2();
		ShutdownAll();
	}
};

class Network: public Actor{
//...
	OnOp1(const Op1&){
		auto v = InNetworkThread();
		if(v) Send(ui, Op1{});
		else Send(writer, Finally{});
	}
};

class UI: public Actor{
//...
	OnOp1(const Op1&){
		InUIThread();
		Send(writer, Finally{});
	}
};


И кстати в таком виде мы не только избегаем лапшевидного кода, но и делаем разделение кода по областям. Т.е. скажем человек занимающийся программированием UI сможет видеть и редактировать весь «свой» код в одном файле, а не искать его по всем лямбдам проекта.
На мой вкус — не лучше лапши.

При программировании на ASIO всегда раньше были упрёки «У нас 1 действие разваливается на кучу мелких шагов, разбросанных по исходникам. Не видна последовательность шагов при беглом взгляде». Почему при прграммировании с акторами это считается плюсом — мне не ведомо.

Поставьте ваш пример рядом с финальным результатом с сопрограмами и сравните, что лучше.
На мой вкус — не лучше лапши.

Ну как же. Тут у нас:
1. Принципиальное отсутствие лапши — добавление функции любой сложности не добавляет ни одного уровня вложенности.
2. Нормальная модульность, которая не просто удобна для разделения работы между разными специалистами, но и позволяет безболезненно менять приложение. К примеру в моём коде я могу без каких-либо сложностей сменить весь UI код (скажем решив поменять используемую GUI библиотеку), в то время как сделать аналогичное, при размазанном по всему проекту UI коду, практически не реально.
3. И данный пример может даже легко поддержать старый интерфейс (для любителей лапши) — достаточно добавить в акторы ещё один обработчик сообщений, принимающий и исполняющий лямбды.

При программировании на ASIO всегда раньше были упрёки «У нас 1 действие разваливается на кучу мелких шагов, разбросанных по исходникам. Не видна последовательность шагов при беглом взгляде». Почему при прграммировании с акторами это считается плюсом — мне не ведомо.

Не стоит смешивать пример из статьи и ASIO, т.к. есть как минимум пара принципиальных различий:
1. Асинхронный код при реализации сервера (работающего со многими клиентами) является необходимость из соображений производительности. В то время как для примера выше он не просто ненужен, а даже менее эффективен (в ситуации когда число одновременных задач сравнимо с числом ядер процессора).
2. В ASIO разбивается на кусочки (и склеивается с помощью сопрограмм) конвейер обработки сетевого запроса. Это не имеет ничего общего с примером выше, в котором смешиваются сетевые запросы, работа с UI и ещё чёрт знает что.

Поставьте ваш пример рядом с финальным результатом с сопрограмами и сравните, что лучше.

Ха, так даже сравнивать нечего. Мой пример — это практически законченный код, в который осталось добавить конструкторы акторов (в которых задаётся соответствие между типом обрабатываемого сообщения и конкретной функцией-обработчиком) и можно компилировать и использовать. А для аналогичной ситуации с кодом на сопрограммах, к нему надо ещё добавить:
— код CoroTask
— код PromiseType
— код WorkQueue.

В итоге мой пример будет выглядеть в разы лаконичнее. )))

P.S. Самое забавное, что изначальный пример статьи по своей сути является реализацией модели акторов, только сильно упрощённой, со всего одним типом сообщений (std::function). Стоит убрать это упрощение и понять, что можно передавать сообщения разных типов, как сразу пропадает необходимость в лямбдах (и соответственно порождаемая их вложенностью лапша).
Если задача из статьи напоминает вашу любимую задачу, это ещё не значит что ваше любимое решение подходит.

Можно решать задачу через акторов, можно через сопрограммы, можно через сигналы, можно «лапшой», можно через visitor, можно через монады… Что выбрать — зависит от множества факторов. В статье про сопрограммы даётся пример, что можно решать через сопрограммы.

Про Акторов
Расскажите разработчику, что ему надо реализовать 1 маленькую функцию UpdateMap, которая «если надо — обновляет карту по сети, отрисовывает её пользователю и сохраняет на диск». При этом все нужные методы уже написаны в сторонних библиотеках.

Звучит как задача для стажёра.

А потом уточните, что решать надо через акторов, да ещё самому этих акторов писать, разбивая UpdateMap на части, оборачивая сторонние библиотеки под каждый чих, и прописывая функционал в разных файлах.

Это уже задача не для стажёра.

Вам придётся объяснять что делать, как делать и что такое акторы. Особенно много вам придётся рассказывать о передаче в акторов данных, которые в примере захватываются через [=].


Если вы так уверены в безоговорочной победе акторов, давайте всё же сравним полные реализации. Напишите пожалуйста полный рабочий пример с акторами, за основу возьмите код из статьи
Можно решать задачу через акторов, можно через сопрограммы, можно через сигналы, можно «лапшой», можно через visitor, можно через монады… Что выбрать — зависит от множества факторов.

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

В статье про сопрограммы даётся пример, что можно решать через сопрограммы.

А вот здесь не согласен. Потому как без проблем можно забивать гвозди с помощью тяжёлого гаечного ключа, одна вряд ли описание подобного процесса будет идеальным гайдом по использованию гаечных ключей…

Если вы так уверены в безоговорочной победе акторов, давайте всё же сравним полные реализации. Напишите пожалуйста полный рабочий пример с акторами, за основу возьмите код из статьи

Ну вообще то, как я уже говорил, код выше — это практически всё что надо. Ему осталось добавить только конструкторы, main и include'ы. Однако если так уж хочется взглянуть на полный компилируемый пример, то накидаю на днях.
Sign up to leave a comment.