Programación funcional en C++: nivel avanzado
La programación funcional (PF) es un paradigma en el que los cálculos se representan como la evaluación de funciones matemáticas, evitando cambios de estado y datos mutables. Aunque C++ fue originalmente un lenguaje orientado a objetos y procedural, a partir del estándar C++11 recibió potentes herramientas para PF: expresiones lambda, std::function, std::bind, y en C++17 se agregó soporte para std::optional y std::variant, que permiten implementar patrones mónadicos. En este artículo exploraremos técnicas avanzadas de programación funcional en C++: composición de funciones, currying, trabajo con mónadas y creación de funciones puras utilizando las capacidades modernas del lenguaje.
1. Expresiones lambda y funciones puras
Las expresiones lambda son la base del estilo funcional en C++. Permiten crear funciones anónimas en el lugar, lo que es ideal para algoritmos de <algorithm>. Sin embargo, para una verdadera PF es importante que las lambdas sean puras — es decir, que no tengan efectos secundarios ni dependan de estado mutable externo.
#include <iostream>#include <vector>#include <algorithm>
int main() { std::vector<int> numbers = {1, 2, 3, 4, 5};
// Lambda pura: solo datos de entrada, solo salida auto square = [](int x) { return x * x; };
// Aplicamos transform sin efectos secundarios 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;}Para uso avanzado, se debe evitar la captura por referencia ([&]) a menos que sea crítico. La captura por valor ([=]) o la enumeración explícita de capturas hace que la lambda sea más predecible y testeable.
2. Composición de funciones con std::function y operadores
La composición es la combinación de dos o más funciones en una. En C++ se puede implementar composición usando plantillas y std::function. Veamos un compositor genérico:
#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
// Composición de múltiples funciones auto triple = compose(multiplyByTwo, compose(addOne, addOne)); std::cout << triple(5) << std::endl; // ((5+1)+1)*2 = 14 return 0;}Para cadenas más complejas se puede usar el operador de tubería (similar a |> en otros lenguajes). En C++20 es posible sobrecargar operator| para crear una sintaxis conveniente:
#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;}Este enfoque hace que el código sea declarativo y fácil de leer.
3. Currying y aplicación parcial
El currying es la transformación de una función de varios argumentos en