LINUX.ORG.RU

Монадические операции: быть или не быть?

 , c++23


0

6

Кто не знал, в C++23 добавили «монадические операции» для std::optional. Вот тут можно посмотреть пример:

https://www.cppstories.com/2023/monadic-optional-ops-cpp23/

Что бы вы предпочли в своем коде: первый вариант (по старинке с if(x) …) или второй, монадический? При условии, разумеется, что вам доступен и разрешен C++23.

Лично я - первый, и вот, почему:

1) во-первых, классический пример уже неоправданно многословен. Зачем писать «if (x) … if (! x) …», когда можно просто «if (x) … else …»;

  1. автор как бы специально хочет показать, что с монадическими код короче, и специально вставляет пустые строки между if’ами, там где с монадическими операциями пишет всё подряд без пустых строк;
  2. приходится на пустом месте создавать лямбды;
  3. а что, если будет два std::optional, и второй используется опционально, при условии, что первый установлен? Тогда забор из лямбд.

Такое ощущение, что делается это всё ради жертвы богу функциональщины.

А вы как считаете?

★★★★★

Последнее исправление: seiken (всего исправлений: 2)

Вообще, вот C#

Это же никакая даже особо не МОНАДА, это тупо null propagation плюс чуток паттерн-матчинга.

public interface IRepository
{
    public int? GetNextAge(int userId = 12345) =>
        (FetchFromCache(userId) ?? FetchFromServer(userId)) switch
        {
            { } profile => ExtractAge(profile) + 1,
            _ => null
        };

    UserProfile? FetchFromCache(int userId);

    UserProfile? FetchFromServer(int userId);

    int ExtractAge(UserProfile profile);
}

Настоящие монады в C# есть в виде LINQ.

После этого, C++ смотрится как сраный ад. Каким он и является.

А если мы учтем, что тут еще в реальной жизни понадобится, на самом деле, асинхронность(не будем же мы блокируясь ждать данные от кеша и СУБД), и если на все это навесить управление памятью, которые в крестах ручное и в 99% случаев кривое(утечки, фрагментация, сегфолты итд), то возникает совсем печальная картина.

И это при том, что C# компилируется в нейтив код, где вот эта красота просто развернется в парочку джампов при сравнении с нуллом. В итоге дотнет-разработчик уже задеплоит MVP пока плюсист ищет, где же у него течет память, почему тормозят «умные» указатели, и что там на самом деле сегфолтится. В этом вот примере, ага.

Лисперы же, тем временем пока статикодебилы долбятся с монадами, запустят MVP уже на втором проекте:

(defun get-next-age (&optional (user-id 12345))
  (when-let ((profile (or (fetch-from-cache user-id)
                          (fetch-from-server user-id))))
    (1+ (extract-age profile))))

для тех кому даже скобки лень печатать есть вон: https://github.com/phoe/binding-arrows/blob/main/doc/TUTORIAL.md#short-circuiting-threading-macros

(defun get-next-age (&optional (user-id 12345))
  (some-> (or (fetch-from-cache user-id)
              (fetch-from-server user-id))
          extract-age
          1+))

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

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

Лавсанчег только что показал, что он не умеет читать C++ный код. Что не новость, конечно, но сам факт добровольного каминг-аута… :)

В исходном примере extractAge возвращает optional, илитарный вы наш.

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

дурачок, твой сраный optional<int> это Nullable<int> в C#, который там есть со времен очаковских и покоренья крыма.

А в лиспе вообще таким дрочевом заниматься не надо, там nil почти в любом месте может использоваться

lovesan ★★★
()

Depends. Причём в т.ч. тупо по настроению могу в двух соседних строках по-разному запилить.

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

UPD. Вообще тут надо конечно обрести понимание, какие конструкции во что компилируются и как оптимизируются. Может и не так страшен чёрт. Вспоминается видео с какого-то CppCon, когда огроменный детинушка (точнее скорее длинный; явный боксёр или борец) с диким русским акцентом, работвший в clang над корутинами, демонстрировал, как они оптимизируются нафиг вообще в константу.

Тут когда-то кто-то выразил уверенность, что к плюсам и pattern matching рано или поздно прикрутят. Вот тогда будет срач так срач. :)

dimgel ★★★★★
()
Последнее исправление: dimgel (всего исправлений: 2)
const auto ageNext = fetchFromCache(userId)
       .or_else([&]() { return fetchFromServer(userId); })
       .and_then(extractAge)
       .transform([](int age) { return age + 1; });

ЛОЛ КЕК, эти имбецилы решили изобрести новый язык, который работает поверх крестов. Я недавно в проекте одному дурачку навставлял люлей за такие выкрутасы. Вспоминается баян про то, что «любая большая программа на Си содержит в себе интерпретатор ЛИСПа, притом кривой».

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

ЛОЛ КЕК, эти имбецилы решили изобрести новый язык, который работает поверх крестов.

Можно подумать, он первый. :)

Вспоминается баян про то, что «любая большая программа на Си содержит в себе интерпретатор ЛИСПа, притом кривой».

Во, и даже не второй. :)

И вообще, что ты имеешь против eDSL? :)

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

Во, и даже не второй

Дело не в том, что он первый, или шестнадцатый, а в том что это тупая идея, превращать кресты в интерпретируемый язык. Причём интерпретируемый по-дидовски через шитый код.

что ты имеешь против eDSL

В каком месте тут eDSL?

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

каком месте он интерпретируемый

Монада по сути мини-интерпретатор. Где-то конечно компилятор сумеет это статически закомпилять, но в общем случае нет. И даже статически это будет типа

call op1
call op2
call op3

как в шитом коде.

только eL налицо

На лицо надо нассать тем кто это предлагает.

no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 2)

Зашел сказать, что со страшной силой ненавижу людей, которые не понимают, когда уже все, хватит, стоп, достаточно.
Все им, сука, мало, «развитие» им подавай.
А TIMWTDI в ЯП это зло, ошибка, говно и диверсия.

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

А TIMWTDI в ЯП это зло, ошибка, говно и диверсия.

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

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

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

Вот именно. :) Даже на «не совсем ЯП» TIMWTDI в полный рост.

Вообще, я начал было писать, что когда пишу for() и мне нужен индекс, предпочитаю от n до нуля, т.к. проверка индекса на ноль в скомпилированном коде короче выйдет. А есть ещё range-for. Уже три for() на ровном месте. А есть ещё while(), и выбор между ними зачастую дело стиля. Или настроения.

dimgel ★★★★★
()
Последнее исправление: dimgel (всего исправлений: 1)

История о том как превратить cmp jump call в call jump call jump call jump call jump call jump call обычно в скриптах такое норм лиспы там всякие, джавы, джиесы. Асинхронная сетевая лапша калбечная или асинхронная реактивная лапша, но там коннвееры по сути обработки данных, скушал обработал,выкакал кушать другому и так далее, а тут тупо условия на вызовы менять. ЦеПусПус хочет быть всем походу :D

LINUX-ORG-RU ★★★★★
()
Ответ на: комментарий от lovesan

Это же никакая даже особо не МОНАДА, это тупо null propagation

Если придерживаться математического определения монады, как математической структуры с операциями return и bind, удовлетворяющими определенным условиям, то null propagation - это монада с операциями

return: T -> T?

и

bind: T? -> (T -> U?) -> U?

А LINQ позволяет создавать произвольные монады.

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

bind: T? -> (T -> U?) -> U?

И где там это?

Нету.

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

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

в исходном примере опциональный результат

std::optional<int> extractAge(const UserProfile& profile);
{ } profile => ExtractAge(profile) + 1,

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

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)

приходится на пустом месте создавать лямбды;

В создании лямбд нет ничего страшного, если они не сохраняются в какой-нибудь std::function, а используются разу по месту, то после оптимизации, скорее всего, никаких накладных расходов не будет.

Меня лично в коде с or_else/and_then/transform больше смущает то, что здесь неочевидно когда и как типы преобразуются из одного в другой. Т.е. мы имеем:

const auto ageNext = fetchFromCache(userId)
    .or_else([&]() { return fetchFromServer(userId); })
    .and_then(extractAge)
    .transform([](int age) { return age + 1; });

при этом fetchFromCache и fetchFromServer возвращают optional, а лямбда в transform возвращает просто int. При этом ageNext у нас будет не int, а optional. Т.е. где-то в этой цепочке произойдет замена optional на optional. И, при этом, в этой цепочке где-то еще и будет учитываться, что optional может таки оказаться пустым.

Т.е. читая эту короткую запись придется расшифровывать все операции, что-то вроде:

fetchFromCache(userId) // Тут optional<UserProfile>, может быть и пустой.
  .or_else(...) // Сюда пойдем только если optional<UserProfile> был пуст.
                // На выходе тоже будет optional<UserProfile>.
                // Но если fetchFromCache вернул не пустой optional,
                // то сюда не пойдем, но у нас все еще optional<UserProfile>.
  .and_then(...) // Сюда попадаем либо "сразу" после fetchFromCache, либо же
                 // после fetchFromServer. На входе у нас optional<UserProfile>,
                 // на выходе же уже optional<int>.
  .transform(...) // Сюда попадаем только если extractAge вернул не пустой optional.
                  // Если же extractAge вернул пустой optional, то сюда не идем и
                  // и остаемся с пустым значением.
                  // Если же extractAge что-то вернул, то лямбда в transform
                  // возвращает int, который автоматически заворачивается в
                  // optional<int> и у нас таки optional<int>.

Т.е. для меня проблема в том, что операции or_else и transform подразумевают скрытые if-ы, которые приходится реконструировать в голове, да еще и дополнять картинку преобразованием типов.

Так что код на or_else/and_them/transform лаконичнее, конечно же. Но, с непривычки, точно не проще и не очевиднее.

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

И где там это? Нету.

Если псевдокодом, то bind nullable f = nullable?.f()

это бред собачий […] не имеющими практически никакого отношения к реальности происходящего в коде

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

Это абстракция, но иногда полезная абстракция.

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

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

Тогда как в исходном варианте с if-ами все это на поверхности.

Но, может быть, это с непривычки.

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

можно вместо auto сразу написать optional

Я на скале почти всегда явно прописывал конечный тип таких цепочек (особенно если они строк на 10-20-30), для самоконтроля и читабельности (особенно для читабельности-по-диагонали).

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

Это абстракция, но иногда полезная абстракция.

Ага, есть еще такая вещь как continuations, теперь че, всем надо думать в терминах first-class continuations? Хаскельнутые совсем ошизели.

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

советуют не злоупотреблять auto

Они правы. И могу объяснить почему. Есть нюансы. Относительно классический пример - арифметика на rationals. Представьте что у Вас промежуточный результат любой мат операции ненормализован, и сокращение происходит в момент каста к rational. Ну, идею Вы поняли, я надеюсь…

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

Там в C++ это навернуто для того чтобы все запихать в optional, наверняка, без лишнего кода. По факту, IRL, в C# это вообще скорее всего было бы свойство Age у объекта UserProfile, которое вместе с ним вытаскивается из базы или кеша.

Но цена вопроса - да, в C# вопросик поставить у типа возвращаемого значения.

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

В создании лямбд нет ничего страшного, если они не сохраняются в какой-нибудь std::function, а используются разу по месту, то после оптимизации, скорее всего, никаких накладных расходов не будет.

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

Откуда только программы на крестах так тормозят? Да от этого же и тормозят. IRL нагромождение говнокода и говноабстракций на C++ особо сильно не оптимизируешь, там инвариантов тех же - хер да маленько, и те что есть обходить можно, но это так, лирика, типичная крестомакака даже не поймет о чем речь.

В C# вон можно посмотреть конкретный ассемблерный выхлоп.

https://godbolt.org/z/ad7WzPds9

Не совсем такой как в JIT будет, но похоже. На лиспе - функция disassemble прямо из коробки есть.

Крестошизики ассемблерный листинг своих высеров ессно никогда не смотрят.

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

Откуда только программы на крестах так тормозят?

Я видел очень быстрый код на Джаве, и очень медленный код на плюсах. «Вы не котиков не любите, Вы их готовить не умеете»…

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

IRL говно на крестах оптимизировать невозможно, потому что там идет обмазывание шаблонами(«modern» C++) в которых хер пойми что происходит, там постоянные indirect ссылки на ссылки(привет промахи кеша), ну и конечно смартпоинтеры повсюду, внутри которых опять ссылки на ссылки на ссылки и атомарные операции (читай блокировка кеш-линии итд), и плюс копирование всего и вся повсюду, и плюс всратый менеджмент памяти(привет фрагментация, привет malloc или даже куда более всратые самописные аллокаторы итд).

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

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

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

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

Говно на крестах это примерно любой проект на крестах.

IRL всякие браузеры там, видеоигры, и прочее. Которое течет памятью, глючит, тормозит и сегфолтится.

Где же эти гении крестовые которые прямо тюнят каждую мелочь и разбираются в ассемблерных листиках? Непонятно. Вернее понятно, их нет, и никто на крестах таким не занимается. Занимаются святой верой в компилятор и вообще «ну ето же сипласплас он по дефолту быстрый, и там нету VM и GC, которые фууу и тормозят»

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

Говно на крестах это примерно любой проект на крестах.

Улыбнулся…

Занимаются святой верой в компилятор

Хех, Вы так и не поняли как оно работает?

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

IRL говно на крестах оптимизировать невозможно, потому что там идет обмазывание шаблонами(«modern» C++) в которых хер пойми что происходит, там постоянные indirect ссылки на ссылки(привет промахи кеша), ну и конечно смартпоинтеры повсюду,

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

в плюсах можно и без смартпоинтеров писать. никто не запрещает. а всех недовольных посылать, если оно правильно написано.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)

Язык C++, конечно, всегда был нечитаемым говном, но сейчас его с особым цинизмом превращают в херобору галактического масштаба.

Такое впечатление, что комитет безудержно хочет за счет языка умещать больше конструкций на меньшем куске экрана, и это возведено в культ и за каким-то хреном называется «выразительностью». Я не удивлюсь, если в погоне за утрамбовкой смыслов на квадратный сантиметр площади, эти деятели в конце концов придумают символьные обозначения для LZW-упаковки, и заставят всех в голове распаковывать код из символьного представления запакованных данных. Или вдруг решат, что неплохо было бы писать не только слева-направо, но и писать конструкции по спирали (блин, уже реализовано)... Ну ладно, решат что код должен изгибаться как лабиринт Sidewinder, или, что еще лучше, по аттрактору Лоренца. Просто потому, что по их мнению это красиво и выразительно.

Вот это плохо, ведь поймет любой школьник, как так можно:

int main() {
    const int userId = 12345;
    std::optional<int> ageNext;

    auto profile = fetchFromCache(userId);
    
    if (!profile)
        profile = fetchFromServer(userId);

    if (profile) {
        auto age = extractAge(*profile);
        
        if (age)
            ageNext = *age + 1;
    }

А вот это харащо:
int main() {
    const int userId = 12345;

    const auto ageNext = fetchFromCache(userId)
        .or_else([&]() { return fetchFromServer(userId); })
        .and_then(extractAge)
        .transform([](int age) { return age + 1; });
}

Вместо чтения будем ломать себе мозг лямбдами и замыканиями упиханными dotchaining функции ради того, чтобы сократить код на два символа, и показать этим зарсанцам что программирование - это вам не хрен собачий!

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

не используй ненужные шаблоны.

Я вообще C++ не использую. Стараюсь, по крайней мере. Но вон адепты modern C++ их столько высирают, что это компилируется по 5 часов - че стоит один boost - его любителей вообще надо, по-моему, на кол сажать.

«смартпоинтеры» - во всех языках со продвинутой сборкой мусора.

Нет конечно. Ну, если мы не берем Python и прочее наколеночное говно типа Golang, а нормальные tracing GC, в которых там mark-region алгоритмы, или вариации generational copying коллекторов итд.

то есть там и ссылки считают

Конечно нет, настоящие GC никакие ссылки не считают.

и обходом мусор собирают и пулы всякие устраивают, если делают уплотнение или перемещение,

«Слышал звон, не знаю где он».

то «ссылка на ссылку» неизбежна.

Естественно, нет. Вон что в лиспе(SBCL итд), что в дотнете(в случае SZArray) - например одномерный массив это:

[ {метка типа} | {размер} |...данные...]

Все лежит в одном куске памяти. На него соответственно, напрямую указывают указатели на этот массив. Часто указывают даже прямо на данные, особенно если указатели тегированные(как в лиспе).

Теперь ради интереса советую посмотреть в std::vector, в то как это говно устроено.

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

У шизиков крестанутых вечно вера в святой компилятор, и оптимизации всего до нуля.

Может потому, что компиляторы таки могут это делать? (gcc-13.2 кстати, пока не смог).

Откуда только программы на крестах так тормозят?

Да потому что криворукие программисты берут в руки инструмент, который не в состоянии освоить и, например, делают повторный поиск в std::map:

std::string rn(bd.Name);
ResourceVar* rv = NULL;
if(rm->find(rn) != rm->end())
  rv = rm->at(rn);

тогда как достаточно было сделать:

ResourceVar* rv = NULL;
{
  ResourceMap::iterator it = rm->find(std::string(bd.Name));
  if(it != rm->end())
    rv = &(it->second);
}

И кто же автор сего фрагмента? Может ему таки в зеркало посмотреть?

eao197 ★★★★★
()

Что бы вы предпочли в своем коде: первый вариант (по старинке с if(x) …) или второй, монадический?

С точки зрения человека который на крестах никогда не писал - первый выглядит читабельнее и понятно что происходит

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

Т.е. аж целый дополнительный язык ввели, чтобы замаскировать естественную и неизбежную императивность операций ввода-вывода под якобы-иммутабельность. А то, что сама программа на интерпретируемом языке иммутабельна – нифига не достижение: обычная императивная программа в ELF-файле и в его образе в памяти тоже, знаете ли, как правило иммутабельна.

Божественно.

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

Я подозреваю, что там код еще на С++98/03 написан (таки 10 лет назад лавсанчег это кропал), nullptr только в C++11 появился. Ну и у тех, кто до сих пор вплотную только с WinAPI и COM работает, в коде до сих пор NULL-ов полно, т.к. традиция-с, копипаста-с и прочее-с…

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

ResourceMap::iterator it = rm->find(std::string(bd.Name));

Кстати говоря, в современных плюсиках (которые хотя бы C++17) можно даже без создания этого временного std::string обойтись. Что-то вроде:

using ResourceMap = std::map<std::string,ResourceVar*, std::less<>>;
...
ResourceMap::iterator it = rm->find(std::string_view(bd.Name));

Что будет еще эффективнее, если размер bd.Name не позволяет задействовать SSO.

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

лавсанчег у нас из старой школы. я уж 20 лет назад писал в хидере

#pragma once

вместо жутких придумываний гардпеременной.

короче он в неудобном каком-то «диалекте» плюсов пишет. и ругается всяко

опять же переопределяя вирт методы базового класса НАДО писать override. иначе возможны неуловимые ошибки в имени метода.

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

там инвариантов тех же - хер да маленько, и те что есть обходить можно, но это так, лирика, типичная крестомакака даже не поймет о чем речь.

Крестошизики ассемблерный листинг своих высеров ессно никогда не смотрят.

В C# вон можно посмотреть конкретный ассемблерный выхлоп.

Но шаропмакаки, конечно, смотрят ассемблерный выхлоп, «и понимают, о чём речь».

seiken ★★★★★
() автор топика