Функциональное программирование в C++: продвинутые техники и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Функциональное программирование в 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 с простыми проверками, но понимание монадической структуры помогает писать более безопасный и декларативный код, особенно при работе с ошибками или асинхронными операциями.

Блоги

Рекомендаций по книгам