LINUX.ORG.RU

CPS в нефункциональных ЯП

 ,


0

3

Вопрос тем, кто использует (изначально, преимущественно) не функциональные ЯП (C++, Java, Python). Как вы относитесь к CPS (Continuation-Passing Style)?

Для тех, кто не в курсе, например, есть некая операция, которая может завершиться удачно, а может с ошибкой, и вместо:

Result performOperation()
{
	return doOperation();
}

...

Result result = performOperation();

if (OK == result)
{
	std::cout << "OK" << std::endl;
}
else
{
	std::cout << "NOK" << std::endl;
}

Делается так:

void performOperation(Func onSuccess, Func onFailure)
{
	if (OK == doOperation())
	{
		onSuccess();
	}
	else
	{
		onFailure();
	}
}

...

performOperation([]() {
			std::cout << "OK" << std::endl;
		},
	         []() {
			std::cout << "NOK" << std::endl;
		});

Заранее пардон, если какие-то синтаксические ошибки, это чисто концептуальные примеры.

Даже предлагаю варианты ответов:

  1. Код с этими лямбдами сложно понимать, всегда объявляю все функции и методы по-старинке, вызываю явно в императивном стиле;

  2. Да, только так и делаю. Умные люди придумали ФП не просто так и гораздо нагляднее, не то, что у дурачков с императивной лапшой;

  3. Мне всё равно, могу/делаю и так, и сяк.

★★★★★

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

Ну тут всё-таки значения, а не колбэки. По теме: depends. Для ошибок категорически плюсую исключения. Колбэки у меня – очень редкий сценарий, но если удачно ложатся, то почему нет? А набор колбэков это по сути стратегия в ООП.

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

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

ox55ff ★★★★★
()

std::expected (C++23) с монадическими методами:

std::expected<int, std::string> fn1(int v) {
    if (v < 0) {
        return std::unexpected("Value too small");
    }

    return v * 2;
}

std::expected<int, std::string> fn2(int v);
std::expected<int, std::string> fn3(int v);

void calc(int initialValue) {
    auto result = fn1(initialValue)
        .and_then(fn2)
        .and_then(fn3);
    
    if (result.has_value()) {
        std::cout << "Result: " << result.value() << std::endl;
    } else {
        std::cout << "Error: " << result.error() << std::endl;
    }
}
static_lab ★★★★★
()
Последнее исправление: static_lab (всего исправлений: 1)

1. В перечисленных языках есть исключения.

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

3. Длинные лямбды используют латентные перлооднострочникофилы.

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

но ИМХО рассматривать такие штуки в отрыве от конкретной задачи особого смысла не имеет.

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

Т.о., в идеальном случае, происходит такая последовательность операций:

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

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

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

Исключения - нечитаемое говно

Если бы проблема была только в читаемости. Конкретно в C++ это ещё и огромные грабли в виде утечек памяти после рефакторинга, препятствие к оптимизации кода компилятором (типичный случай когда с -fno-exceptions какой-нибудь конструктор инлайнится, а с включенными уже нет), а ещё дикая головная боль при написании lock-free кода (нужно обмазываться смарт-пойнтерами там, где без исключений они были бы на хрен не нужны)

annulen ★★★★★
()

в приведённом утрированном примере лямбда(колбек) вообще «не пришей кобыле хвост», она там просто вредна и нечего не решает. По крайней мере в С/С++ подобных..

В точности не скажу (возможно в Эрланге), там где массовая параллельность, там наверное стоит всех сразу оповестить что Ж"па и можно свой микро-тред убить. Вот только там OnFailure() вполне

для С/С++ колбек/лямбда OnFailure() не решает самой главной задачи - что отдать вверх по стеку вызовов. Локальные действия ты предпринял и надо возвращать управление, а там ждут корректных объектов. И как быть ? в OnFailure выставить глобальный флаг и повсюду проверяться??

то есть исключения это хорошо. Если не перебарщивать

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

MKuznetsov ★★★★★
()

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

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

Длинные однострочники на перле с преимущественно спецсимволами-операторами - самое плохое в мире

Если сравнивать с развесистыми однострочниками на sed или, ещё хуже, кривыми конвейреами с дикой смесью sed, grep, awk и cut, по внешнему виду которых вообще не понятно, что они делают, однострочники на перле покажутся манной небесной. А если выучить перл, то и не покажутся, а будут.

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

Портируете callback-hell из Javascript?

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

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

для С/С++ колбек/лямбда OnFailure() не решает самой главной задачи - что отдать вверх по стеку вызовов. Локальные действия ты предпринял и надо возвращать управление, а там ждут корректных объектов. И как быть ? в OnFailure выставить глобальный флаг и повсюду проверяться??

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

CPS же, это не про обработку ошибок, это как раз про некий аналог std::expected. То есть в onSuccess надо не вывод ОК, а всю остальную программу запихивать. Но любой язык без оптимизации хвостовых вызовов, просто уронит стек. Впрочем, и с оптимизацией: cl-cont на CPS-преобразовании затормаживает выполнение программы на Common Lisp в десятки раз.

Наверное, единственный язык, где это работает — Haskell. В остальных, если вдруг необходим CPS, значит надо переходить или на язык с нормальными продолжениями (Scheme, Racket, вроде Ruby) или на Haskell.

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

CPS же, это не про обработку ошибок, это как раз про некий аналог std::expected. То есть в onSuccess надо не вывод ОК, а всю остальную программу запихивать. Но любой язык без оптимизации хвостовых вызовов, просто уронит стек. Впрочем, и с оптимизацией: cl-cont на CPS-преобразовании затормаживает выполнение программы на Common Lisp в десятки раз.

когда так, если колбек __Cdecl и attribute((noreturn)) и он действительно «noreturn» то разницы почти нет. Всё равно что большая вложенность. Рухнет ли стек будет зависить от насколько глубоко в функции следующий такой вызов и как компилятор поступает с локальными переменными при «noreturn».

Сфера применения такого приёма не очень большая. Даже с лёту не придумаю где бы мне это пригодилось :-) Может кодо-генерация, сгенерировать A(B(C(D))) мизерно проще чем if (A) if (B) if (C) D ; и при генерации «наверху» сложно сформулировать условие/ алгоритм, о нём только «внизу» знают..

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

и как компилятор поступает с локальными переменными при «noreturn».

Именно. И что в Си++ происходит с деструкторами.

Сфера применения такого приёма не очень большая.

Реальное применение я знаю одно: корутины без потоков. Ещё есть сохранение состояния программы, но тут проще память процесса записать.

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

При нормальных продолжениях ещё удобно сайты писать.

Можно делать код типа

если авторизация() тогда
  стартовая-страница()
  основной-экран()

И получить три экрана сайта.

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

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

monk ★★★★★
()

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

не является это «функциональным стилем», непонятно в чем сыр-бор.

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

Дело в том, что в моем случае вся многопоточность и асинхронщина уже сделана на уровне фреймворка явно, есть некие логические модули, у каждого есть очередь сообщений для всех коммуникаций с другими модулями. Задача прикладного программиста лишь реализовывать интерфейсы с коллбеками. Все в лучших традициях C++ эпохи 2000х. Никаких там актор моделей и yield городить не только не нужно, но и противопоказано.

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