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.
