Programación funcional en C#: de la sintaxis a la arquitectura
\n\nEl C# moderno ha trascendido hace tiempo los límites de un lenguaje puramente orientado a objetos. Desde la versión 3.0 (LINQ) hasta las últimas versiones de .NET, el lenguaje ha ido absorbiendo activamente los paradigmas de la programación funcional (PF). Si ya dominas las lambdas y LINQ, pero quieres profundizar más, este artículo es para ti. Analizaremos conceptos avanzados de PF que harán tu código en C# más predecible, testeable y conciso.
\n\n1. Funciones puras y manejo de efectos secundarios
\n\nUna función pura es una función que:
\n- \n
- Siempre devuelve el mismo resultado para los mismos argumentos (transparencia referencial). \n
- No tiene efectos secundarios (no modifica el estado global, no escribe en archivos, no envía solicitudes HTTP). \n
En C#, un ejemplo perfecto de función pura son las operaciones matemáticas. El problema es que una aplicación real no puede consistir solo de funciones puras: tarde o temprano necesitarás guardar datos o enviar un correo. La solución es separar la lógica de los efectos.
\n\nTécnica avanzada: utiliza funtores y mónadas para contener los efectos. Veamos un ejemplo simple con Maybe<T> (análogo a nullable, pero funcional):
public class Maybe<T>\n{\n private readonly T _value;\n public bool HasValue { get; }\n \n private Maybe(T value, bool hasValue) => (_value, HasValue) = (value, hasValue);\n \n public static Maybe<T> Some(T value) => new Maybe<T>(value, true);\n public static Maybe<T> None() => new Maybe<T>(default, false);\n \n // Functor: Select (Map)\n public Maybe<TResult> Select<TResult>(Func<T, TResult> selector) =>\n HasValue ? Maybe<TResult>.Some(selector(_value)) : Maybe<TResult>.None();\n \n // Mónada: SelectMany (FlatMap/Bind)\n public Maybe<TResult> SelectMany<TResult>(Func<T, Maybe<TResult>> binder) =>\n HasValue ? binder(_value) : Maybe<TResult>.None();\n}\n\n// Uso\nMaybe<int> result = Maybe<int>.Some(10)\n .Select(x => x * 2)\n .SelectMany(x => x > 15 ? Maybe<int>.Some(x) : Maybe<int>.None());\n\nEste código no genera excepciones ni verifica null manualmente: toda la lógica de manejo de ausencia de valor está encapsulada en la mónada.
2. Currying y aplicación parcial
\n\nEn la PF clásica, cada función toma un solo argumento. El currying es la transformación de una función con varios parámetros en una cadena de funciones anidadas, cada una tomando un solo argumento. La aplicación parcial es la fijación de una parte de los argumentos.
\n\nEn C# no hay currying incorporado, pero es fácil de implementar mediante closures:
\n\n// Función normal\nFunc<int, int, int, int> add = (a, b, c) => a + b + c;\n\n// Versión curry\nFunc<int, Func<int, Func<int, int>>> curriedAdd = a => b => c => a + b + c;\n\n// Aplicación parcial: fijamos el primer argumento\nFunc<int, Func<int, int>> add5 = curriedAdd(5);\n\n// Uso\nint sum = add5(10)(20); // 35\n\nEscenario avanzado: inyección de dependencias mediante aplicación parcial. En lugar de pasar la configuración o el servicio a través del constructor de la clase, puedes fijarlos en la etapa de composición:
\n\npublic class ReportService\n{\n public Func<string, string, string> FormatReport => \n (template, data) => $"[{DateTime.Now}] {template}: {data}";\n}\n\n// Uso con aplicación parcial\nvar service = new ReportService();\nvar formatWithTemplate = service.FormatReport("Error"); // fijamos la plantilla\nstring errorMessage = formatWithTemplate("Connection failed");