Índice

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

  1. Escribe una función aplicar_a_todos(arr: Array, f: Fn) -> Array que aplique una función a cada elemento y devuelva un nuevo array. Pruébala con al menos tres funciones diferentes.
  1. Escribe contar_coincidencias(arr: Array, predicado: Fn) -> int que cuente cuántos elementos satisfacen una función predicado.
  1. Escribe componer(f: Fn, g: Fn, x: int) -> int que aplique g primero, luego f: componer(f, g, x) es igual a f(g(x)).
  1. Crea un closure crear_multiplicador(factor: int) -> Fn que devuelva una función que multiplique su argumento por factor. Úsalo para crear funciones triple y cuadruple.
  1. Escribe encontrar_primero(arr: Array, predicado: Fn) -> int que devuelva el primer elemento que cumpla el predicado, o -1 si ninguno cumple.

Resumen

Siguiente capítulo: Enums y pattern matching →

← Anterior: Archivos Siguiente: Enums y pattern matching →