Programación funcional en Go: del imperativo a la declaratividad
Go (Golang) tradicionalmente se asocia con la programación imperativa y concurrente, sin embargo, su potente sistema de tipos y el soporte para funciones de primera clase hacen posible aplicar muchos principios de la programación funcional (PF). En este artículo, analizaremos cómo aplicar modismos de PF en Go sin sacrificar el rendimiento ni la legibilidad del código.
Principios fundamentales de PF en el contexto de Go
La programación funcional en Go no significa abandonar por completo el estado mutable. En su lugar, utilizamos los siguientes conceptos:
- Funciones puras — funciones cuyo resultado depende únicamente de los argumentos, sin efectos secundarios.
- Inmutabilidad (Immutability) — preferencia por crear nuevas estructuras de datos en lugar de modificar las existentes.
- Funciones de orden superior — funciones que toman otras funciones como argumentos o las devuelven.
- Clausuras (Closures) — funciones que capturan variables del ámbito externo.
Funciones de orden superior: map, filter, reduce
En Go no hay genéricos incorporados (hasta la versión 1.18), por lo que los clásicos map, filter, reduce se implementan mediante interfaces o copia explícita. Veamos un ejemplo con números enteros:
package main
import "fmt"
// Map — aplica una función a cada elemento del slicefunc Map(s []int, fn func(int) int) []int { result := make([]int, len(s)) for i, v := range s { result[i] = fn(v) } return result}
// Filter — conserva solo los elementos que cumplen la condiciónfunc Filter(s []int, fn func(int) bool) []int { var result []int for _, v := range s { if fn(v) { result = append(result, v) } } return result}
// Reduce — reduce el slice a un solo valorfunc Reduce(s []int, init int, fn func(int, int) int) int { acc := init for _, v := range s { acc = fn(acc, v) } return acc}
func main() { nums := []int{1, 2, 3, 4, 5}
// Duplicamos cada elemento doubled := Map(nums, func(x int) int { return x * 2 }) fmt.Println("Map:", doubled) // [2 4 6 8 10]
// Conservamos solo los pares evens := Filter(nums, func(x int) bool { return x%2 == 0 }) fmt.Println("Filter:", evens) // [2 4]
// Sumamos todos los elementos sum := Reduce(nums, 0, func(acc, x int) int { return acc + x }) fmt.Println("Reduce:", sum) // 15}Importante: En Go 1.18+ con genéricos se pueden crear versiones universales:
// Generic Mapfunc MapGeneric[T, U any](s []T, fn func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = fn(v) } return result}Clausuras y currying en Go
Las clausuras en Go son una herramienta poderosa para crear funciones que mantienen estado. Veamos un ejemplo de un generador de secuencias:
package main
import "fmt"
// Generador de la secuencia de Fibonaccifunc fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a }}
// Currying: función que toma argumentos por etapasfunc add(a int) func(int) int { return func(b int) int { return a + b }}
func main() { fib := fibonacci() fmt.Println(fib()) // 1 fmt.Println(fib()) // 1 fmt.Println(fib()) // 2
// Currying add5 := add(5) fmt.Println(add5(10)) // 15 fmt.Println(add5(20)) // 25}Las clausuras son especialmente útiles para:
- Crear funciones configurables (middleware)
- Implementar el patrón "fábrica de funciones"
- Organizar mecanismos de callback