Translate

viernes, 2 de enero de 2026

Métodos Monádicos en Go — Parte 2


En el post anterior definimos Result[T], un tipo genérico para representar operaciones que pueden fallar, junto con las funciones Map, FlatMap y OrElse.

Ahora vamos a llevar ese patrón al mundo concurrente, donde los errores y la sincronización suelen volverse un caos si no se estructuran bien.

Go facilita lanzar tareas concurrentes con goroutines, pero mezclar concurrencia y manejo de errores puede ser tedioso:


go func() {

    v, err := doSomething()

    if err != nil {

        // manejar error...

    }

    res, err := doSomethingElse(v)

    if err != nil {

        // otro error...

    }

    // ...

}()


Cada paso necesita su if err != nil, y los canales deben sincronizar resultados y errores manualmente.

Podemos crear una versión concurrente del patrón Result, donde cada operación devuelve un canal que emite un Result[T].


func Async[T any](f func() Result[T]) <-chan Result[T] {

    ch := make(chan Result[T], 1)

    go func() {

        defer close(ch)

        ch <- f()

    }()

    return ch

}


Creamos versiones asíncronas de Map y FlatMap:


func MapAsync[A, B any](in <-chan Result[A], f func(A) B) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        ch <- Map(r, f)

    }()

    return ch

}


func FlatMapAsync[A, B any](in <-chan Result[A], f func(A) <-chan Result[B]) <-chan Result[B] {

    ch := make(chan Result[B], 1)

    go func() {

        defer close(ch)

        r := <-in

        if r.Err != nil {

            ch <- Err[B](r.Err)

            return

        }

        out := f(r.Value)

        ch <- <-out

    }()

    return ch

}


Supongamos que tenemos operaciones concurrentes que pueden fallar:


func FetchData() Result[int] {

    time.Sleep(time.Millisecond * 100)

    return Ok(10)

}


func Compute(x int) Result[int] {

    return Ok(x * 2)

}


func SaveResult(x int) Result[string] {

    if x > 15 {

        return Ok(fmt.Sprintf("Saved %d", x))

    }

    return Err[string](fmt.Errorf("too small"))

}


Podemos encadenarlas de forma limpia:


result := FlatMapAsync(

    MapAsync(Async(FetchData), Compute),

    func(x int) <-chan Result[string] { return Async(func() Result[string] { return SaveResult(x) }) },

)


fmt.Println(OrElse(<-result, "failed")) // "Saved 20"

Las ventajas son :

  • Las tareas se ejecutan en goroutines separadas.
  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • No hay if err != nil, ni sincronización manual.


Podés crear fácilmente un combinador All para ejecutar varias tareas concurrentes que devuelvan Result:


func All[T any](tasks ...func() Result[T]) Result[[]T] {

    ch := make(chan Result[T], len(tasks))

    for _, f := range tasks {

        go func(fn func() Result[T]) { ch <- fn() }(f)

    }


    var results []T

    for i := 0; i < len(tasks); i++ {

        r := <-ch

        if r.Err != nil {

            return Err[[]T](r.Err)

        }

        results = append(results, r.Value)

    }

    return Ok(results)

}


Si alguna tarea falla, el error se devuelve inmediatamente.

Si todas tienen éxito, se devuelven los resultados combinados.


El enfoque monádico aplicado a Go te permite combinar funciones puras y goroutines sin perder control del flujo ni del manejo de errores.

Aporta claridad a sistemas concurrentes y simplifica el código de coordinación.

Métodos Monádicos en Go


Go no usa palabras como mónada, pero su estilo basado en funciones y retorno explícito de errores se adapta perfectamente a la idea de encadenar operaciones que pueden fallar, sin perder claridad.

Con los genéricos (desde Go 1.18), podemos escribir funciones reutilizables que se comportan como map, flatMap, orElse de otros lenguajes.


El estilo Go para manejar errores:


value, err := DoSomething()

if err != nil {

    return 0, err

}

result, err := DoSomethingElse(value)

if err != nil {

    return 0, err

}

return result, nil


Funciona bien, pero escala mal cuando hay muchos pasos encadenados.

Podemos mejorarlo aplicando ideas monádicas.

Vamos a representar una operación que puede tener éxito o error:


type Result[T any] struct {

    Value T

    Err   error

}


Y creamos constructores simples:


func Ok[T any](v T) Result[T]   { return Result[T]{Value: v} }

func Err[T any](e error) Result[T] { return Result[T]{Err: e} }


Aplica una función al valor, si no hay error.


func Map[A, B any](r Result[A], f func(A) B) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return Ok(f(r.Value))

}


Aplica una función que también devuelve un Result, y lo aplana.


func FlatMap[A, B any](r Result[A], f func(A) Result[B]) Result[B] {

    if r.Err != nil {

        return Err[B](r.Err)

    }

    return f(r.Value)

}


OrElse: Devuelve un valor alternativo si hubo error.


func OrElse[T any](r Result[T], fallback T) T {

    if r.Err != nil {

        return fallback

    }

    return r.Value

}


Y como lo usamos? 


func ParseInt(s string) Result[int] {

    n, err := strconv.Atoi(s)

    if err != nil {

        return Err[int](err)

    }

    return Ok(n)

}


func DivideByTwo(n int) Result[int] {

    if n%2 != 0 {

        return Err[int](fmt.Errorf("odd number"))

    }

    return Ok(n / 2)

}


func main() {

    result := FlatMap(ParseInt("42"),

        func(n int) Result[int] {

            return Map(DivideByTwo(n), func(x int) int { return x * 3 })

        })


    fmt.Println(OrElse(result, 0)) // 63

}

¿Cuáles son las ventajas?

  • Si ocurre un error en cualquier paso, se propaga automáticamente.
  • Sin if err != nil en cada línea.
  • Código más funcional y declarativo.


Aunque Go no tenga sintaxis monádica ni azúcar funcional, su modelo basado en valores de retorno y funciones puras encaja perfectamente con la idea.

Con un par de funciones genéricas, podés escribir código más declarativo y mantenible.


jueves, 1 de enero de 2026

Métodos Monádicos en C# parte 3


En el post anterior, implementamos una clase Result<T> con métodos monádicos (Map, FlatMap, OrElse).

Ahora combinaremos esa idea con Task<T> para obtener un flujo asíncrono y seguro ante errores, sin try/catch y sin anidar await.

Supongamos que tenemos funciones asíncronas que pueden fallar:


Task<Result<User>> GetUserAsync(int id);

Task<Result<Order>> GetOrderAsync(User user);

Task<Result<Invoice>> CreateInvoiceAsync(Order order);


Queremos encadenarlas de forma limpia, propagando el error automáticamente, sin esto 👇:


var userResult = await GetUserAsync(id);

if (userResult is ErrorResult<User>) return userResult;


var orderResult = await GetOrderAsync(userResult.Value);

// etc...


Creamos extensiones que aplanan el contexto doble (Task<Result<T>>):


public static class TaskResultExtensions

{

    public static async Task<Result<U>> Map<T, U>(

        this Task<Result<T>> task, Func<T, U> f)

    {

        var result = await task;

        return result is OkResult<T> ok ? Result<U>.Ok(f(ok.Value)) 

                                        : Result<U>.Error(((ErrorResult<T>)result).Message);

    }


    public static async Task<Result<U>> FlatMap<T, U>(

        this Task<Result<T>> task, Func<T, Task<Result<U>>> f)

    {

        var result = await task;

        return result is OkResult<T> ok ? await f(ok.Value) 

                                        : Result<U>.Error(((ErrorResult<T>)result).Message);

    }

}


Veamos como usarlo: 


var invoice = await GetUserAsync(10)

    .FlatMap(GetOrderAsync)

    .FlatMap(CreateInvoiceAsync)

    .Map(invoice => invoice.WithDiscount(10))

    .FlatMap(SaveInvoiceAsync)

    .OrElse(Result<Invoice>.Error("No se pudo generar factura"));


¿Cuál es la ventaja?

  • Si cualquier paso falla, la cadena se corta automáticamente
  • No se necesitan try/catch ni comprobaciones manuales
  • El código se lee de arriba a abajo, como una secuencia lógica de pasos


Veamos otro ejemplo:


public async Task<Result<string>> GenerateInvoice(int userId)

{

    return await GetUserAsync(userId)

        .FlatMap(GetOrderAsync)

        .FlatMap(CreateInvoiceAsync)

        .Map(invoice => invoice.Id.ToString());

}


Si alguna función devuelve un Result.Error, ese error se propaga sin ejecutar los pasos siguientes.

El resultado final es un Result<string> con éxito o mensaje de error.