El paradigma monádico permite encadenar operaciones que transforman valores sin salir del contexto en el que viven: puede ser una secuencia, una operación asíncrona o un valor que puede no existir.
En C#, estas ideas se encuentran en estructuras muy familiares.
Una mónada encapsula un valor junto con su contexto:
- puede faltar → Nullable<T> o Option<T>
- varios valores → IEnumerable<T>
- asíncrono → Task<T>
Lo importante es que provea dos operaciones fundamentales:
- Map (o en C# Select) — aplica una función al valor dentro del contexto
- FlatMap (o en C# SelectMany) — aplica una función que devuelve otro contenedor del mismo tipo y lo aplana
Nullable<T> — el valor opcional
Cualquier tipo T puede transformarse en Nullable<T> usando T?.
C# no expone métodos como map o flatMap, pero podemos simularlos con extensiones.
public static class NullableExtensions
{
public static TResult? Map<T, TResult>(this T? value, Func<T, TResult> f)
where T : struct where TResult : struct =>
value.HasValue ? f(value.Value) : (TResult?)null;
public static TResult? FlatMap<T, TResult>(this T? value, Func<T, TResult?> f)
where T : struct where TResult : struct =>
value.HasValue ? f(value.Value) : (TResult?)null;
}
Uso:
int? x = 5;
int? result = x.Map(n => n * 2).FlatMap(n => n > 5 ? n : null);
Console.WriteLine(result); // 10
No hay if (x.HasValue) explícitos — todo se encadena naturalmente.
IEnumerable<T> — mónada de listas
LINQ es, de hecho, una sintaxis monádica.
Los métodos Select y SelectMany son equivalentes a map y flatMap.
var result = from x in new[] { 1, 2, 3 }
from y in new[] { 10, 20 }
select x + y;
foreach (var r in result)
Console.WriteLine(r);
SelectMany combina múltiples secuencias y aplana el resultado.
Esto es exactamente la mónada lista del álgebra funcional.
Task<T> — mónada asíncrona
En C#, Task<T> encapsula una operación que producirá un valor en el futuro.
Los métodos ContinueWith, await, o Task.WhenAll son sus operaciones monádicas.
Task<int> getValue = Task.FromResult(5);
var result = await getValue
.ContinueWith(t => t.Result * 2)
.ContinueWith(t => t.Result + 3);
Console.WriteLine(result.Result); // 13
await actúa como un flatMap implícito, “saca” el valor del contexto y permite encadenar cálculos asíncronos de forma natural.
Y podemos definir nuestras propias monadas pero eso es para otro post...
