Closures y funciones de primera clase
Funciones como valores
En la Parte 1, aprendiste a definir y llamar funciones. Pero en Nyx, las funciones son más poderosas que eso — son valores, igual que los enteros o los strings. Puedes almacenar una función en una variable, pasarla a otra función, o devolverla desde una función.
Este concepto se llama funciones de primera clase, y desbloquea toda una nueva forma de pensar sobre los programas.
Pasar funciones a funciones
Empecemos con un ejemplo simple. Supón que tienes dos funciones que transforman un número:
fn doble(n: int) -> int { return n * 2 } fn cuadrado(n: int) -> int { return n * n }
Ahora quieres una función que aplique cualquier transformación a un número:
fn aplicar(f: Fn, x: int) -> int { return f(x) } fn main() { print(aplicar(doble, 5)) // 10 print(aplicar(cuadrado, 5)) // 25 }
El tipo Fn significa "una función." Pasas doble y cuadrado por nombre — sin paréntesis, porque no las estás llamando, las estás pasando.
¿Por qué es útil?
Imagina que quieres aplicar una transformación a cada elemento de un array. Sin funciones de primera clase, necesitarías escribir un bucle separado para cada transformación. Con ellas:
fn doble(n: int) -> int { return n * 2 } fn negar(n: int) -> int { return 0 - n } fn sumar_diez(n: int) -> int { return n + 10 } fn mapear_array(arr: Array, f: Fn) -> Array { var resultado: Array = [] var i: int = 0 while i < arr.length() { resultado.push(f(arr[i])) i += 1 } return resultado } fn main() { let nums: Array = [1, 2, 3, 4, 5] let duplicados: Array = mapear_array(nums, doble) print(duplicados) // [2, 4, 6, 8, 10] let negados: Array = mapear_array(nums, negar) print(negados) // [-1, -2, -3, -4, -5] let desplazados: Array = mapear_array(nums, sumar_diez) print(desplazados) // [11, 12, 13, 14, 15] }
Una sola función (mapear_array) maneja los tres casos. El comportamiento cambia según qué función pases.
Filtrar y reducir
Dos patrones más que funcionan muy bien con funciones de primera clase:
fn es_par(n: int) -> bool { return n % 2 == 0 } fn es_positivo(n: int) -> bool { return n > 0 } fn filtrar_array(arr: Array, predicado: Fn) -> Array { var resultado: Array = [] var i: int = 0 while i < arr.length() { if predicado(arr[i]) { resultado.push(arr[i]) } i += 1 } return resultado } fn sumar(a: int, b: int) -> int { return a + b } fn multiplicar(a: int, b: int) -> int { return a * b } fn reducir_array(arr: Array, inicial: int, f: Fn) -> int { var acc: int = inicial var i: int = 0 while i < arr.length() { acc = f(acc, arr[i]) i += 1 } return acc } fn main() { let nums: Array = [1, 2, 3, 4, 5, 6, 7, 8] let pares: Array = filtrar_array(nums, es_par) print(pares) // [2, 4, 6, 8] let suma: int = reducir_array(nums, 0, sumar) print(suma) // 36 let producto: int = reducir_array(nums, 1, multiplicar) print(producto) // 40320 }
Estos tres — map, filter, reduce — son los bloques fundamentales del procesamiento de datos en muchos lenguajes.
¿Qué es un closure?
Un closure es una función que recuerda variables del ámbito donde fue definida. En Nyx, creas closures definiendo funciones dentro de otras funciones:
fn crear_sumador(n: int) -> Fn { fn sumar(x: int) -> int { return n + x } return sumar } fn main() { let sumar5: Fn = crear_sumador(5) let sumar10: Fn = crear_sumador(10) print(sumar5(3)) // 8 print(sumar10(3)) // 13 print(sumar5(100)) // 105 }
Cuando crear_sumador(5) se ejecuta, crea una función sumar que recuerda n = 5. Incluso después de que crear_sumador retorna, la función interna todavía tiene acceso a n. Ese entorno capturado es lo que la convierte en un closure.
Los closures capturan variables
Los closures pueden capturar e incluso modificar variables de su ámbito envolvente:
fn contador() -> int { var cuenta: int = 0 fn incrementar() { cuenta = cuenta + 1 } fn obtener_cuenta() -> int { return cuenta } incrementar() incrementar() incrementar() return obtener_cuenta() } fn main() { print(contador()) // 3 }
Tanto incrementar como obtener_cuenta comparten la misma variable cuenta. Cuando incrementar la modifica, obtener_cuenta ve el valor actualizado.
Closures como acumuladores
fn acumular(n: int) -> int { var total: int = 0 fn sumar_al_total(x: int) { total = total + x } var i: int = 1 while i <= n { sumar_al_total(i) i = i + 1 } return total } fn main() { print(acumular(10)) // 55 (1+2+3+...+10) print(acumular(5)) // 15 (1+2+3+4+5) }
Aplicar funciones múltiples veces
fn aplicar_dos_veces(f: Fn, x: int) -> int { return f(f(x)) } fn aplicar_n_veces(f: Fn, x: int, n: int) -> int { var resultado: int = x var i: int = 0 while i < n { resultado = f(resultado) i += 1 } return resultado } fn doble(n: int) -> int { return n * 2 } fn incrementar(n: int) -> int { return n + 1 } fn main() { print(aplicar_dos_veces(doble, 3)) // 12 (3*2=6, 6*2=12) print(aplicar_n_veces(doble, 1, 10)) // 1024 (2^10) print(aplicar_n_veces(incrementar, 0, 100)) // 100 }
Ejemplo práctico: ordenamiento con comparador
Puedes usar funciones de primera clase para hacer un ordenamiento genérico:
fn ordenar_array(arr: Array, comparar: Fn) -> Array { // Bubble sort simple var resultado: Array = [] var i: int = 0 while i < arr.length() { resultado.push(arr[i]) i += 1 } var hubo_cambio: bool = true while hubo_cambio { hubo_cambio = false i = 0 while i < resultado.length() - 1 { if comparar(resultado[i], resultado[i + 1]) > 0 { let temp: int = resultado[i] resultado[i] = resultado[i + 1] resultado[i + 1] = temp hubo_cambio = true } i += 1 } } return resultado } fn ascendente(a: int, b: int) -> int { return a - b } fn descendente(a: int, b: int) -> int { return b - a } fn main() { let nums: Array = [5, 2, 8, 1, 9, 3] let asc: Array = ordenar_array(nums, ascendente) print(asc) // [1, 2, 3, 5, 8, 9] let desc: Array = ordenar_array(nums, descendente) print(desc) // [9, 8, 5, 3, 2, 1] }
Mismo algoritmo de ordenamiento, comportamiento diferente según la función que pases.
Ejemplo práctico: callbacks de eventos
Los closures son perfectos para patrones orientados a eventos:
fn al_superar_umbral(valores: Array, umbral: int, callback: Fn) { var i: int = 0 while i < valores.length() { if valores[i] > umbral { callback(valores[i]) } i += 1 } } fn alerta(valor: int) { print("ALERTA: valor " + int_to_string(valor) + " supera el umbral!") } fn main() { let temperaturas: Array = [22, 35, 28, 41, 19, 38, 45] al_superar_umbral(temperaturas, 37, alerta) }
Salida:
ALERTA: valor 41 supera el umbral! ALERTA: valor 38 supera el umbral! ALERTA: valor 45 supera el umbral!
Ejercicios
- Escribe una función
aplicar_a_todos(arr: Array, f: Fn) -> Arrayque aplique una función a cada elemento y devuelva un nuevo array. Pruébala con al menos tres funciones diferentes.
- Escribe
contar_coincidencias(arr: Array, predicado: Fn) -> intque cuente cuántos elementos satisfacen una función predicado.
- Escribe
componer(f: Fn, g: Fn, x: int) -> intque apliquegprimero, luegof:componer(f, g, x)es igual af(g(x)).
- Crea un closure
crear_multiplicador(factor: int) -> Fnque devuelva una función que multiplique su argumento porfactor. Úsalo para crear funcionestripleycuadruple.
- Escribe
encontrar_primero(arr: Array, predicado: Fn) -> intque devuelva el primer elemento que cumpla el predicado, o-1si ninguno cumple.
Resumen
- Las funciones son valores en Nyx — puedes pasarlas, almacenarlas y devolverlas.
- El tipo
Fnrepresenta un valor de función. - Pasa una función por nombre (sin paréntesis):
aplicar(doble, 5). - Map, filter y reduce son patrones poderosos construidos sobre funciones de primera clase.
- Un closure es una función que captura variables de su ámbito envolvente.
- Los closures pueden leer y modificar variables capturadas.
- Usa closures para crear funciones con estado incorporado (contadores, sumadores, multiplicadores).
Siguiente capítulo: Enums y pattern matching →