La programación funcional es un paradigma que construye programas mediante la composición de funciones y limita los cambios de estado y los efectos secundarios. Durante años se presentó como algo académico, oscuro y reservado para lenguajes como Haskell o Lisp.
Pero algo cambió. Los procesadores dejaron de ser más rápidos para tener más núcleos. La concurrencia se volvió crítica. Y de repente, el estado mutable de la OOP se convirtió en una pesadilla de gestionar.
Hoy en día, lenguajes como C#, Java, JavaScript y C++ han abrazado características funcionales. No para reemplazar a la OOP, sino para complementarla.
En este artículo no vamos a hablar de mónadas ni funtores. Vamos a centrarnos en código predecible y fácil de probar.
Funciones puras
El concepto central de la FP es la Función Pura. Para que una función sea considerada pura, debe cumplir estrictamente dos reglas:
- Determinismo: Dados los mismos argumentos de entrada, siempre devuelve el mismo resultado.
- Sin Efectos Secundarios (Side Effects): No modifica nada fuera de su propio ámbito (no cambia variables globales, no escribe en disco, no imprime en consola).
Ejemplo en C#
// ❌ FUNCIÓN IMPURA
// Depende de una variable externa 'tasaImpuesto'.
// Si alguien cambia 'tasaImpuesto', el resultado cambia aunque el input sea igual.
public decimal CalcularTotalImpuro(decimal monto)
{
return monto * GlobalConfig.tasaImpuesto;
}
// ❌ FUNCIÓN IMPURA (Efecto secundario)
// Modifica el estado del sistema (Console) y depende del tiempo (DateTime.Now)
public void LogMensaje(string mensaje)
{
Console.WriteLine($"{DateTime.Now}: {mensaje}");
}
// ✅ FUNCIÓN PURA
// Todo lo que necesita entra por parámetros.
// No toca nada fuera. Siempre devuelve lo mismo para 100 y 0.21.
public decimal CalcularTotalPuro(decimal monto, decimal tasa)
{
return monto * tasa;
}
¿Por qué nos importa esto?
Las funciones puras son sencillas de probar. No necesitas mocks ni configurar bases de datos: llamas a CalcularTotalPuro(100, 0.21) y compruebas que devuelve 21. También puedes evaluarlas de forma concurrente sin coordinar estado compartido, siempre que los valores con los que trabajan sean realmente inmutables.
Inmutabilidad
En OOP tradicional, creamos objetos y los modificamos constantemente (setters). En FP, los datos no cambian. Si quieres cambiar algo, creas una copia nueva con el cambio aplicado.
Esto parece ineficiente a primera vista (“¿copiar todo el objeto?”), pero en arquitecturas modernas ayuda muchísimo a evitar Race Conditions. Si un objeto no puede cambiar, dos hilos pueden leerlo simultáneamente sin miedo a que uno lo corrompa.
Inmutabilidad en C# con records
C# 9 introdujo los record. Los records posicionales ofrecen propiedades init y una sintaxis cómoda para crear copias, aunque un record no garantiza inmutabilidad profunda: puede contener miembros mutables.
// Definición de un tipo inmutable
public record Persona(string Nombre, int Edad);
// Uso
var p1 = new Persona("Luis", 30);
// ❌ ERROR: No se puede cambiar la propiedad (es init-only)
// p1.Edad = 31;
// ✅ CORRECTO: "Non-destructive mutation" usando 'with'
// Creamos una NUEVA instancia copiando p1 pero cambiando la edad
var p2 = p1 with { Edad = 31 };
Console.WriteLine(p1.Edad); // 30 (p1 sigue intacto)
Console.WriteLine(p2.Edad); // 31
Funciones de orden superior
En FP, las funciones son ciudadanos de primera clase. Esto significa que una función puede ser:
- Asignarse a una variable.
- Pasarse como argumento a otra función.
- Devolverse como resultado de otra función.
Esto nos permite abstraer no solo datos, sino comportamiento.
Func<T, R> y Action<T>
En C#, usamos delegados genéricos para esto. Veamos cómo crear un método que puede filtrar cualquier cosa, delegando la lógica de decisión al que llama.
public class Filtrador
{
// Esta función recibe una lista Y OTRA FUNCIÓN (predicado)
public List<int> Filtrar(List<int> numeros, Func<int, bool> criterio)
{
var resultado = new List<int>();
foreach (var n in numeros)
{
// Ejecutamos la función que nos han pasado
if (criterio(n))
{
resultado.Add(n);
}
}
return resultado;
}
}
// Uso:
var lista = new List<int> { 1, 2, 3, 4, 5, 6 };
var filtrador = new Filtrador();
// Pasamos el comportamiento "ser par" como argumento (Lambda)
var pares = filtrador.Filtrar(lista, n => n % 2 == 0);
// Pasamos el comportamiento "ser mayor que 3"
var mayores = filtrador.Filtrar(lista, n => n > 3);
Esto es exactamente lo que hace LINQ (.Where(), .Select()). LINQ es una librería funcional incrustada en un lenguaje orientado a objetos.
Composición de funciones
Si nuestras funciones son pequeñas piezas de Lego (Puras), podemos encadenarlas para crear procesos complejos.
En matemáticas, si tienes f(x) y g(x), puedes hacer h(x) = g(f(x)).
En programación funcional, esto se llama Pipeline.
// Imperativo (Anidado y difícil de leer)
var resultado = GuardarEnBD(ConvertirAJson(ValidarUsuario(usuario)));
// Funcional (Extensions methods en C# permiten simular pipelines)
// El dato fluye de izquierda a derecha (o de arriba a abajo)
var resultado = usuario
.Validar()
.ConvertirAJson()
.GuardarEnBD();
¿OOP o FP? Podemos combinar ambas
A menudo se presentan como enemigos, pero en el desarrollo moderno (especialmente en C#, TypeScript, Swift o Kotlin), la arquitectura ideal es híbrida:
- Usa OOP para modelar objetos con identidad y responsabilidades: organiza el código en módulos cohesivos y define límites claros.
- Usa FP para las transformaciones: aprovecha la inmutabilidad, LINQ y las funciones puras, y separa los efectos secundarios.
La Programación Funcional te obliga a ser disciplinado. Al restringir lo que puedes hacer (no mutar variables, no tener efectos secundarios ocultos), paradójicamente te da más libertad: la libertad de refactorizar sin miedo y de paralelizar tu código sin dolores de cabeza.
No hace falta que reescribas todo tu código mañana. Puedes empezar aislando cálculos deterministas y pasando sus dependencias como parámetros. Un método static no es puro por ser estático; todavía puede leer la hora, escribir en disco o modificar estado global.
Haz que el núcleo de tu dominio sea Funcional (Puro e Inmutable) y empuja los efectos secundarios (BBDD, UI, Red) hacia los bordes de la aplicación. Esto se conoce como Functional Core, Imperative Shell.