Функциональное программирование в C++: продвинутый уровень
Функциональное программирование (ФП) — это парадигма, рассматривающая вычисления как вычисление математических функций и избегающая изменяемого состояния и побочных эффектов. Хотя C++ традиционно ассоциируется с объектно-ориентированным и процедурным программированием, современные стандарты (C++11 и новее) внедрили мощные инструменты для написания функционального кода. В этой статье мы выйдем за рамки базовых лямбд и рассмотрим продвинутые техники: от композиции функций до эмуляции монад.
1. Композиция функций и конвейеры
Композиция функций — это процесс объединения двух или более функций для создания новой функции. В C++ для этого удобно использовать лямбда-выражения и шаблоны.
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
// Шаблон для композиции двух функций
template<typename F, typename G>
auto compose(F f, G g) {
return [f, g](auto x) {
return f(g(x));
};
}
// Пример использования
int main() {
auto addTwo = [](int x) { return x + 2; };
auto multiplyByThree = [](int x) { return x * 3; };
// Композиция: сначала multiplyByThree, потом addTwo
auto composed = compose(addTwo, multiplyByThree);
std::cout << composed(5) << std::endl; // (5*3) + 2 = 17
// Конвейерная обработка (pipe) с помощью std::ranges (C++20)
std::vector<int> numbers = {1, 2, 3, 4, 5};
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int n : result) {
std::cout << n << " "; // 4 16
}
return 0;
}
Конвейеры с std::views (C++20) позволяют строить цепочки преобразований без создания промежуточных контейнеров, что является классическим функциональным подходом.
2. Каррирование (Currying) и частичное применение
Каррирование — это трансформация функции, принимающей несколько аргументов, в последовательность функций, каждая из которых принимает один аргумент. Частичное применение фиксирует часть аргументов.
#include <iostream>
#include <functional>
// Каррированная функция сложения
auto curriedAdd = [](int a) {
return [a](int b) {
return [a, b](int c) {
return a + b + c;
};
};
};
// Частичное применение с помощью std::bind
int multiply(int a, int b, int c) {
return a * b * c;
}
int main() {
// Использование каррирования
auto add5 = curriedAdd(5);
auto add5And3 = add5(3);
std::cout << add5And3(2) << std::endl; // 10
// Частичное применение через std::bind
using namespace std::placeholders;
auto multiplyBy2 = std::bind(multiply, _1, _2, 2);
std::cout << multiplyBy2(3, 4) << std::endl; // 3*4*2 = 24
// Частичное применение через лямбду (более идиоматично в C++14+)
auto multiplyBy3 = [](int a, int b) { return multiply(a, b, 3); };
std::cout << multiplyBy3(5, 6) << std::endl; // 5*6*3 = 90
return 0;
}
Каррирование особенно полезно при работе с функциями высшего порядка, такими как std::transform или std::accumulate, позволяя создавать специализированные версии функций "на лету".
3. Функторы, аппликативы и эмуляция монад
В Haskell монады — это мощный паттерн для работы с побочными эффектами. В C++ мы можем эмулировать их с помощью шаблонов классов. Рассмотрим простую монаду Maybe (аналог std::optional), но с монадическими операциями.
#include <iostream>
#include <functional>
#include <optional>
// Монада Maybe (обертка над std::optional)
template<typename T>
class Maybe {
std::optional<T> value;
public:
Maybe() : value(std::nullopt) {}
Maybe(T v) : value(v) {}
// fmap (функтор) — применяет функцию к значению, если оно есть
template<typename F>
auto fmap(F f) const -> Maybe<decltype(f(*value))> {
if (value) {
return Maybe<decltype(f(*value))>(f(*value));
}
return Maybe<decltype(f(*value))>();
}
// bind (монадический оператор >>=)
template<typename F>
auto bind(F f) const -> decltype(f(*value)) {
if (value) {
return f(*value);
}
return decltype(f(*value))();
}
// Вывод значения (для демонстрации)
void print() const {
if (value) std::cout << *value << std::endl;
else std::cout << "Nothing" << std::endl;
}
};
// Функция, возвращающая Maybe
Maybe<int> safeDivide(int a, int b) {
if (b == 0) return Maybe<int>();
return Maybe<int>(a / b);
}
int main() {
Maybe<int> m1(10);
Maybe<int> m2(0);
// Цепочка монадических операций
auto result1 = m1.bind([&](int x) {
return safeDivide(x, 2).bind([&](int y) {
return Maybe<int>(y + 3);
});
});
result1.print(); // 8 (10/2 + 3)
auto result2 = m2.bind([&](int x) {
return safeDivide(x, 2).bind([&](int y) {
return Maybe<int>(y + 3);
});
});
result2.print(); // Nothing (деление на ноль)
return 0;
}
На практике в C++ чаще используют std::optional с простыми проверками, но понимание монадической структуры помогает писать более безопасный и декларативный код, особенно при работе с ошибками или асинхронными операциями.