Функциональное программирование в C++: продвинутый уровень
Функциональное программирование (ФП) — это парадигма, в которой вычисления представляются как вычисление математических функций, избегая изменения состояния и изменяемых данных. Хотя C++ изначально был объектно-ориентированным и процедурным языком, начиная со стандарта C++11 он получил мощные инструменты для ФП: лямбда-выражения, std::function, std::bind, а в C++17 добавилась поддержка std::optional и std::variant, которые позволяют реализовывать монадические паттерны. В этой статье мы рассмотрим продвинутые техники функционального программирования на C++: композицию функций, каррирование, работу с монадами и создание чистых функций с помощью современных возможностей языка.
1. Лямбда-выражения и чистые функции
Лямбда-выражения — основа функционального стиля в C++. Они позволяют создавать анонимные функции на месте, что идеально для алгоритмов из <algorithm>. Однако для настоящего ФП важно, чтобы лямбды были чистыми — то есть не имели побочных эффектов и не зависели от внешнего изменяемого состояния.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// Чистая лямбда: только входные данные, только выход
auto square = [](int x) { return x * x; };
// Применяем transform без побочных эффектов
std::vector<int> squared;
std::transform(numbers.begin(), numbers.end(),
std::back_inserter(squared), square);
for (int n : squared) std::cout << n << " "; // 1 4 9 16 25
return 0;
}
Для продвинутого использования стоит избегать захвата по ссылке ([&]), если это не критично. Захват по значению ([=]) или явное перечисление захватов делает лямбду более предсказуемой и тестируемой.
2. Композиция функций с std::function и операторами
Композиция — это объединение двух или более функций в одну. В C++ можно реализовать композицию с помощью шаблонов и std::function. Рассмотрим обобщённый композитор:
#include <functional>
#include <iostream>
template <typename F, typename G>
auto compose(F f, G g) {
return [f, g](auto x) {
return f(g(x));
};
}
int main() {
auto addOne = [](int x) { return x + 1; };
auto multiplyByTwo = [](int x) { return x * 2; };
auto addThenMultiply = compose(multiplyByTwo, addOne);
std::cout << addThenMultiply(5) << std::endl; // (5+1)*2 = 12
// Композиция нескольких функций
auto triple = compose(multiplyByTwo, compose(addOne, addOne));
std::cout << triple(5) << std::endl; // ((5+1)+1)*2 = 14
return 0;
}
Для более сложных цепочек можно использовать конвейерный оператор (наподобие |> в других языках). В C++20 появилась возможность перегружать operator| для создания удобного синтаксиса:
#include <functional>
#include <iostream>
template <typename F>
class Pipeable {
F f;
public:
Pipeable(F func) : f(func) {}
template <typename T>
auto operator()(T x) const { return f(x); }
};
template <typename T, typename F>
auto operator|(T x, Pipeable<F> p) {
return p(x);
}
int main() {
auto addOne = Pipeable([](int x) { return x + 1; });
auto square = Pipeable([](int x) { return x * x; });
int result = 5 | addOne | square; // (5+1)^2 = 36
std::cout << result << std::endl;
return 0;
}
Такой подход делает код декларативным и легко читаемым.
3. Каррирование и частичное применение
Каррирование — преобразование функции от нескольких аргументов в последовательность функций от одного аргумента. В C++ это можно реализовать через вложенные лямбды или через std::bind. Рассмотрим современный способ с помощью шаблонов и auto (C++14):
#include <iostream>
#include <functional>
// Каррированная функция сложения
auto curry_add = [](int a) {
return [a](int b) {
return [a, b](int c) {
return a + b + c;
};
};
};
int main() {
auto add5 = curry_add(5);
auto add5and3 = add5(3);
std::cout << add5and3(2) << std::endl; // 5+3+2 = 10
// Частичное применение через std::bind
auto multiply = [](int x, int y, int z) { return x * y * z; };
auto multiplyBy2 = std::bind(multiply, 2, std::placeholders::_1, std::placeholders::_2);
std::cout << multiplyBy2(3, 4) << std::endl; // 2*3*4 = 24
return 0;
}
Каррирование особенно полезно при работе с алгоритмами, где нужно передать частично настроенную функцию, или при создании фабрик функций.
4. Монады и функторы: std::optional и std::expected
Монада — это абстракция, позволяющая последовательно применять функции к значениям, обёрнутым в контекст (например, возможное отсутствие значения или ошибку). В C++ монадический подход реализуется через std::optional (C++17) и std::expected (C++23, доступен в экспериментальной реализации).
Рассмотрим пример с std::optional и функцией and_then (аналог flatMap):
#include <iostream>
#include <optional>
#include <string>
// Монадический and_then для optional
template <typename T, typename F>
auto and_then(std::optional<T> opt, F f) -> decltype(f(*opt)) {
if (opt) return f(*opt);
return std::nullopt;
}
std