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)

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

Потому что там в первом if (!x) иксу присваивается значение. Второй if проверяет что значение таки присвоилось (fetchFromServer может вернуть пустой optional).

И если забыть про второй if, то можем получить UB при дереференсе пустого optional’а.

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

Первый вариант, естественно. И вообще, всем любителям «modern C++» которые пихают в код говно вот такого рода как во втором варианте, надо руки отрубать.

lovesan ★★
()

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

std::optional<some_data> v = try_acquire_value();
if(v) {
  ... // Какой-то код.
  v->something = ...;
  ... // Какой-то код.
}

в процессе развития случается, что v->something = выезжает из под if-а:

std::optional<some_data> v = try_acquire_value();
if(v) {
  ... // Какой-то код.
}
v->something = ...;
... // Какой-то код.

и вот тогда-то обращение к содержимому v стреляет по ногам. Благо, на тестировании это быстро выявляется, но осадочек все же остается :)

Поэтому мне бы лично пригодился бы простой then:

std::optional<some_data> v = try_acquire_value();
v.then([](auto & value) {
  ... // Какой-то код.
  value.something = ...;
  ... // Какой-то код.
});

Надобности монадических and_then, or_else и transform пока в своем коде особо не наблюдал. Но пусть будут. Вдруг лет через 10, когда представится возможность переехать на C++23, потребуются :)))

eao197 ★★★★★
()

Мне первое понятнее (без монадок). Особенно, если переписать его так:

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

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

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

    if (ageNext)
        std::cout << std::format("Next year, the user will be {} years old", *ageNext);
    else
        std::cout << "Failed to determine user's age." << std::endl;
}
Beewek ★★
()

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

auto age = fetch_from_cache(id) || fetch_from_server(id);

и все прекрасно сработает без всяких монадок.

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

А там еще вспоминай, что эти or_else / and_then возращают

Ну да, вот было бы здорово, если бы or_else и and_then имели понятные имена. Вот if на if-е сразу понятно.

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

Виноват, перестарался. Тогда могу предложить только пару auto для паритета со вторым примером:

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

    auto profile = fetchFromCache(userId); 
    if (!profile)
        profile = fetchFromServer(userId);
    
    if (profile)
    {
          if (auto age = extractAge(*profile))
            ageNext = *age + 1;
    }

    if (ageNext)
        std::cout << std::format("Next year, the user will be {} years old", *ageNext);
    else
        std::cout << "Failed to determine user's age." << std::endl;
}
Beewek ★★
()

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

Второй же вариант лаконичнее, легче прослеживается логика, в нём сложнее напортачить (например, забыть ! в условии), его проще оптимизировать компилятору. Но нужно уметь работать с таким кодом, чтобы видеть эти преимущества, или просто быть новичком в программировании, которому ещё не вывихнули мозг императивщиной.

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

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

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

v->something = …;

#include <optional>
#include <iostream>

using namespace std;

struct S
{
    double d = 1.2;
    int i = 3;
};

int main()
{
    std::optional<S> x;
    if (x)
    {
        x->i = 10;
    }

    cout << x->i << endl; // нормально для компилятора, нормально для рантайма, выводит в cout "0", ну и счастливой отладки!
}
seiken ★★★★★
() автор топика
    const auto ageNext = fetchFromCache(userId)
        .or_else([&]() { return fetchFromServer(userId); })
        .and_then(extractAge)
        .transform([](int age) { return age + 1; });
Ехал лямбда через лямбда.
Видит лямбда лямбда в лямбда.

Подобный стиль записи вычислений я использовал в асинхронном коде на JS. Собственно, вот там он вполне оправдан.

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

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

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

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

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

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

Второй же вариант лаконичнее, легче прослеживается логика,

А где логика? Вот мы читаем:

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

ageNext = значение

Или иначе (синтаксическая херня) вернуть другое значение
И тогда переменная
Поменять (объявить переменную, вернуть её + 1)

Логика, ау?

PPP328 ★★★★★
()

специально вставляет пустые строки между if’ами

Есть стандарты кодирования, запрещающие писать тело if и циклов без операторных скобок. Ибо чревато (особенно, когда у того же if появляется else). И все нормальные люди им следуют, уж таким базовым вещам. Так что у меня вопрос почему там не скобки вместо пробела :-)

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

Думаю, C++ не хватает короткого синтаксиса лямбд без return. Действительно, короткие лямбды очень громоздки.

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

Есть стандарты кодирования, запрещающие писать тело if и циклов без операторных скобок. Ибо чревато (особенно, когда у того же if появляется else). И все нормальные люди им следуют, уж таким базовым вещам. Так что у меня вопрос почему там не скобки вместо пробела :-)

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

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

Ты излишне категоричен.

В подобном ФП-стиле мне доводилось много писать на JS и Ruby.

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

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

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

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

Я же говорю, нужно научиться читать такой стиль. ageNext = значение из кеша, если там нет, то из сервера, если есть значение, то extractAge, к полученному прибавить 1.

Но это даже не главное. Главное - нас не интересует промежуточная переменная profile, можно освободить память из-под неё пораньше. А так же легко увидеть, что мы не делаем ничего другого, кроме вычисления ageNext.

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

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

А нефиг писать говнокод. Сразу замечание к этому примеру. Получение profile должно быть вынесено в отдельную функцию. В императивном стиле записано вычисление или в функциональном, не важно, это говнокод в обоих случаях.

Про время жизни переменной и оптимизацию - смешно. Во-первых, любой современный оптимизирующий компилятор определяет время жизни переменных без проблем. Он вообще оптимизирует код не как синтаксическое дерево, а как граф из блоков вычислений, и через это «видит» код совсем иначе, чем программист.

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

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

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

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

Получение profile должно быть вынесено в отдельную функцию.

Так оно и вынесено: fetchFromCache/fetchFromServer. Но в императивном коде нужна промежуточная переменная, которая будет использоваться в if-ах.

Во-первых, любой современный оптимизирующий компилятор определяет время жизни переменных без проблем.

Но не в цпп. Иначе можно выоптимизировать какой-нибудь std::lock_guard напрочь и получить фейерверк багов.

оправдывать стиль кода (каким бы он ни был) преждевременной оптимизацией

Ха, а кто это делал? Я не утверждал, что такой код быстрее. Только что над ним легче проводить рассуждения как человеку (что с моей точки зрения намного важнее), так и компилятору, что потенциально может приводить к лучшей оптимизации последним. И это был лишь один из пунктов «за».

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

Так оно и вынесено: fetchFromCache/fetchFromServer.

То есть нихрена не вынесено. На уровень логики данной функции торчат кишки из логики более низкого уровня.

Иначе можно выоптимизировать какой-нибудь std::lock_guard напрочь и получить фейерверк багов.

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

Выоптимизировать std::lock_guard конечно он не может.

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

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

wandrien ★★
()

Всё это «ФП» в плюсах, конечно, убого и тяжеловесно. Сравнить это с божественными монадами можно только от бедности, впрочем она здесь всегда и была.

Но иногда всё же выходит немного лучше сишной лапши. Как и в сабжевом примере.

Сделают облегченные лямбды (а пытаются) - будет норм. А лучше do-нотацию.

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

оптимизировать точно так же как и эквивалентный императивный код - может

Естественно. Проблема в том, что первый вариант кода не эквивалентен второму. Там есть переменная profile, которая как и lock_guard должна дожить до конца скоупа, просто потому что должна, по крайней мере, если у неё нетривиальный деструктор.

оптимизировать

Забавно, кстати, что при оптимизации используется представление кода SSA, которое близко по духу к функциональщине, над ним проще проводить рассуждения.

unC0Rr ★★★★★
()

читать второй для привыкших к старым стандартам труднее, но он куда более вписывается в современные, особенно в миксе с ranges

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

Это машина по трансформации одних графов императивных вычислений в другие графы императивных вычислений.

Чатгопота с тобой не согласна.

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

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

Мой уточняющий вопрос:

всегда ли машинный код какой-либо реальной программы можно изобразить в виде графов императивных вычислений?

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

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

Графы императивных вычислений обычно используются в анализе и оптимизации программ на уровне компиляторов и других инструментов разработки программного обеспечения, но это абстрактное представление не всегда отражает полную сложность реальных программ в машинном коде.
alex1101
()
Ответ на: комментарий от alex1101

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

Из одного ЯВУ в другой ЯВУ всегда назывался транслятором, чатГПТ не прав.

Дальше чатГПТ (а может быть и ты тоже) демонстрирует неумение работать с контекстом.

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

Графы императивных вычислений обычно используются в анализе и оптимизации программ

об этом и шла речь. Но ты зачем-то сформулировал вопрос в обобщенном виде

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

Из одного ЯВУ в другой ЯВУ всегда назывался транслятором

По-моему, транслятором всегда назывались компиляторы и интерпретаторы вообще. Независимо от того, что во что они там преобразуют.

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

По-моему, транслятором всегда назывались компиляторы и интерпретаторы вообще. Независимо от того, что во что они там преобразуют.

не в моей школе, компилятор не является транслятором

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

Из одного ЯВУ в другой ЯВУ всегда назывался транслятором

От одного автора книги узнал, что если транслятор преобразовывает конкретно ЯВУ в ЯВУ, то он называется транспайлером, но в грамотности автора данной книги я сильно сомневаюсь, да и в интернете чёткого определения транспайлеру так и не нашёл.

С другой стороны компилятор уже содержит в себе транслятор, просто он там работает на уровне подпрограммы.

Dr64h ★★
()

если пользовать std::optional, то вся эта монадическая кухня возникает автоматом, поскольку есть вычисление булевых выражений с полезным сайдэффектом(что соббсно и нужно) на истинности и ложности значения std::optional.

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

зы. а вот если не использовать std::optional, а возвращать из функции bool (успех/неуспех), а результат в «out» параметр, то аналогично можно писать просто на обычном булевом выражении.

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