1. Introducción
Pasando de la teoría a la práctica, es lógico preguntarse: «¿Y para qué necesito, como desarrollador en .NET y C#, todas estas técnicas de programación funcional?»
Es cierto que C# no es un lenguaje puramente funcional como F# o Haskell. Pero desde la versión 3.0 y hasta C# 14 ha incorporado un montón de herramientas de FP que pueden elevar mucho la calidad y expresividad del código.
Aquí es donde funcionan especialmente bien:
- Trabajo con colecciones — LINQ, Map/Reduce, filtrado, agregación, ordenación y otras «magias» con datos.
- Funciones puras — menos bugs por estado y efectos secundarios, más fácil de depurar.
- Funciones de orden superior — componentes genéricos y reutilizables con los que se trabaja con gusto.
- Inmutabilidad en multihilo — una de las claves para código seguro en escenarios paralelos y asíncronos.
- Composición de funciones — hace la lógica de negocio compleja más concisa, legible y fácil de testear.
Tabla: Comparación del enfoque OOP y FP en C#
| Tarea | Imperativo (OOP/escuela antigua) | Funcional (FP) |
|---|---|---|
| Filtrado de lista | |
|
| Transformación de lista | |
|
| Búsqueda por criterio | |
|
| Agregación | |
|
| Cacheo | |
|
2. LINQ: lo más funcional en C#
Si te parece que “programación funcional” va de listas, filtros y cosas como .Where, .Select, .Aggregate, — felicidades: ¡es realmente así! LINQ es la quintaesencia de la FP en C#.
Recordemos cómo funciona LINQ
LINQ opera sobre colecciones mediante cadenas de métodos que aceptan funciones como parámetros (por ejemplo, lambdas). Por ejemplo:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Obtengamos solo los números pares y dupliquémoslos
var result = numbers
.Where(x => x % 2 == 0)
.Select(x => x * 2);
foreach (var number in result)
Console.WriteLine(number);
¿Qué ocurre aquí?
- .Where — función de orden superior: acepta una función (x => x % 2 == 0) y devuelve otra colección.
- .Select — también acepta una función (x => x * 2).
- No modificamos la colección original, sino que obtenemos un nuevo resultado.
Este estilo es fácil de leer y extender (puedes encadenar más .OrderBy, .Take, .Distinct, etc.).
¡Atención! Las expresiones lambda son una forma cómoda de crear un delegado al vuelo. LINQ no habría sido posible sin el soporte de FP en C#.
Esquema: Procesamiento funcional de una colección
Colección --> Where(x => bool) --> Select(x => y) --> Nuevo resultado
3. Composición de funciones y pipeline de procesamiento de datos
En FP se suele usar la composición: una operación compleja se construye como una cadena de pequeñas funciones, cada una con su responsabilidad.
Ejemplo: cadena de procesamiento de una cadena
Estilo con mutación (OOP):
string s = " hello world ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!
Más “funcional” — como pipeline de funciones:
Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";
// Composición de funciones — las aplicamos en secuencia
Func<string, string> pipeline = x => addBang(upper(trim(x)));
Console.WriteLine(pipeline(" hello world ")); // HELLO WORLD!
Combinador simple de composición:
Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
x => g(f(x));
// Y ahora pipeline usando Compose:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);
Console.WriteLine(pipeline2(" hello again ")); // HELLO AGAIN!
4. Trabajo con inmutabilidad: protección contra bugs
La inmutabilidad es un pilar de la FP. No mutamos estructuras de datos, sino que devolvemos nuevas. Esto es especialmente importante en aplicaciones multihilo.
Ejemplo: “mal” (mutable)
List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;
Ejemplo: “bien” (funcional)
var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();
En el C# moderno hay colecciones ImmutableList<T> y otros tipos en el namespace System.Collections.Immutable:
using System.Collections.Immutable;
var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // ¡Devuelve una nueva lista!
5. Funciones de orden superior en la vida real
Las funciones de orden superior son la forma de escribir componentes genéricos sin montones de condicionales.
Ejemplo: filtro genérico para usuarios
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User { Name = "Vasya", Age = 26 },
new User { Name = "Katya", Age = 17 },
new User { Name = "Lyosha", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
return source.Where(u => predicate(u)).ToList();
}
// Uso:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);
6. Pattern matching y expresiones switch
El C# moderno usa mucho pattern matching: las expresiones switch suelen reemplazar cadenas pesadas de if.
object value = 123;
string description = value switch
{
int i when i > 100 => "Número grande",
string s when s.Length > 3 => "Cadena larga",
null => "Valor nulo",
_ => "Desconocido"
};
Console.WriteLine(description); // Número grande
7. Memoización: cacheo de resultados de funciones
Memoización es cachear el resultado de una función para argumentos iguales. En C# se puede implementar fácilmente a mano.
Func<int, int> SlowFib = null; // Función recursiva Fibonacci
var cache = new Dictionary<int, int>();
SlowFib = n =>
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
cache[n] = n;
else
cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
return cache[n];
};
Console.WriteLine(SlowFib(40)); // ¡Relámpago!
8. Currying y aplicación parcial
La aplicación parcial fija parte de los argumentos de una función. En C# es cómodo hacerlo con lambdas.
Func<int, int, int> add = (a, b) => a + b;
// Fijamos el primer argumento
Func<int, int> add10 = b => add(10, b);
Console.WriteLine(add10(5)); // 15
Console.WriteLine(add10(100)); // 110
9. Estilo declarativo con funciones
Imperativo:
var result = new List<int>();
foreach (var n in numbers)
{
if (n > 0)
result.Add(n * n);
}
Declarativo:
var result = numbers
.Where(n => n > 0)
.Select(n => n * n)
.ToList();
10. Tarea práctica
Implementemos un módulo de filtrado de tareas en un «task manager para estudiantes».
Modelo:
class StudentTask
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
public int Priority { get; set; }
}
Datos iniciales:
var tasks = new List<StudentTask>
{
new StudentTask { Title = "Hacer la tarea", IsCompleted = false, Priority = 2 },
new StudentTask { Title = "Tomar café", IsCompleted = true, Priority = 3 },
new StudentTask { Title = "Ver la lección", IsCompleted = false, Priority = 1 }
};
Filtro genérico:
List<StudentTask> FilterTasks(
List<StudentTask> all,
Predicate<StudentTask> predicate)
{
return all.Where(t => predicate(t)).ToList();
}
// Buscar tareas no completadas con prioridad > 1
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);
// Mostrar resultado
foreach (var task in importantTasks)
Console.WriteLine(task.Title);
Combinadores de predicados:
Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;
// Combinamos varios criterios, variante 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));
// Variante 2: función-combinador de dos predicados
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);
var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));
11. Peculiaridades y errores típicos al aplicar el enfoque funcional en C#
Primero, recuerda que C# es un lenguaje fuertemente tipado. A veces necesitas especificar tipos explícitamente, especialmente cuando las funciones devuelven delegados o lambdas complejas. Si no, puedes encontrarte con errores de compilación por tipos no inferidos.
Segundo, no pases funciones con efectos secundarios donde se esperan funciones puras. Modificar variables externas rompe la previsibilidad. Intenta que tus funciones no muten el estado fuera de su scope.
Tercero, ten cuidado con la captura de variables del scope externo (closure capture), sobre todo en código asíncrono y multihilo. Una variable cuyo valor cambia después de pasarla a una lambda (por ejemplo, en LINQ) puede causar bugs no evidentes.
Finalmente, el estilo FP excesivo puede complicar el mantenimiento si el equipo no está acostumbrado. Usa FP donde realmente simplifica la solución, no solo por “lucimiento”.
GO TO FULL VERSION