En esta página

Closures e iteradores: programación funcional en Rust

14 min lectura TextoCap. 5 — Rust en práctica

Closures: funciones anónimas que capturan el entorno

Los closures (clausuras) son funciones anónimas que pueden capturar variables del entorno circundante. Son fundamentales para la programación funcional en Rust y se usan extensamente con los iteradores.

fn main() {
    // Función regular
    fn cuadrado(n: i32) -> i32 { n * n }
    
    // Closure equivalente (inferencia de tipos)
    let cuadrado_cl = |n: i32| -> i32 { n * n };
    
    // Closure con inferencia total
    let cuadrado_inf = |n| n * n;
    
    // Closure de una sola expresión (sin llaves)
    let doble = |n| n * 2;
    
    println!("{}", cuadrado(5));
    println!("{}", cuadrado_cl(5));
    println!("{}", cuadrado_inf(5_i32));
    println!("{}", doble(7_i32));
}

Captura del entorno

A diferencia de las funciones, los closures pueden capturar variables del entorno de tres formas:

fn main() {
    let x = 10;
    let y = String::from("hola");
    
    // Captura por referencia (&T) — lee el valor
    let leer_x = || println!("x = {x}");
    leer_x();
    leer_x(); // Puede llamarse múltiples veces
    println!("x sigue siendo: {x}"); // x sigue disponible
    
    // Captura por referencia mutable (&mut T)
    let mut contador = 0;
    let mut incrementar = || {
        contador += 1;
        println!("Contador: {contador}");
    };
    incrementar();
    incrementar();
    // No puedes usar contador aquí mientras el closure existe
    drop(incrementar);
    println!("Contador final: {contador}"); // Ahora sí
    
    // Captura por movimiento (move keyword)
    let mover_y = move || println!("y capturado: {y}");
    // y ya no está disponible aquí
    mover_y();
}

Los traits Fn, FnMut y FnOnce

Los closures en Rust implementan uno (o más) de estos tres traits:

Trait Descripción Puede llamarse
FnOnce Consume el entorno capturado Solo una vez
FnMut Modifica el entorno capturado Múltiples veces
Fn Lee el entorno capturado Múltiples veces

Todos los closures implementan FnOnce. Si no mueven sus capturas, también implementan FnMut. Si no modifican sus capturas, también implementan Fn.

fn aplicar_una_vez<F: FnOnce() -> String>(f: F) -> String {
    f() // Solo puede llamarse una vez
}

fn aplicar_n_veces<F: FnMut(i32) -> i32>(mut f: F, valor: i32, n: u32) -> i32 {
    let mut resultado = valor;
    for _ in 0..n {
        resultado = f(resultado);
    }
    resultado
}

fn aplicar_veces<F: Fn(i32) -> i32>(f: F, valores: &[i32]) -> Vec<i32> {
    valores.iter().map(|&v| f(v)).collect()
}

fn main() {
    let nombre = String::from("Rust");
    let saludo = move || format!("¡Hola, {nombre}!"); // FnOnce (mueve nombre)
    println!("{}", aplicar_una_vez(saludo));
    
    let resultado = aplicar_n_veces(|n| n * 2, 1, 10);
    println!("2^10 = {resultado}");
    
    let triplicar = |n| n * 3;
    let triplicados = aplicar_veces(triplicar, &[1, 2, 3, 4, 5]);
    println!("{:?}", triplicados);
}

El trait Iterator

El trait Iterator es la interfaz que todos los iteradores implementan:

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    
    // Métodos con implementación por defecto (hay muchos más)
    fn map<B, F: FnMut(Self::Item) -> B>(self, f: F) -> Map<Self, F> { ... }
    fn filter<P: FnMut(&Self::Item) -> bool>(self, predicate: P) -> Filter<Self, P> { ... }
    // ...
}

Puedes implementar tu propio iterador:

struct ContadorHasta {
    actual: u32,
    maximo: u32,
}

impl ContadorHasta {
    fn nuevo(maximo: u32) -> Self {
        ContadorHasta { actual: 0, maximo }
    }
}

impl Iterator for ContadorHasta {
    type Item = u32;
    
    fn next(&mut self) -> Option<u32> {
        if self.actual < self.maximo {
            self.actual += 1;
            Some(self.actual)
        } else {
            None
        }
    }
}

fn main() {
    let contador = ContadorHasta::nuevo(5);
    
    // Como es un Iterator, ¡tienes TODOS los métodos de Iterator gratis!
    let suma: u32 = contador.sum();
    println!("Suma 1 a 5: {suma}");
    
    // Encadenamiento
    let pares_cuadrados: Vec<u32> = ContadorHasta::nuevo(10)
        .filter(|n| n % 2 == 0)
        .map(|n| n * n)
        .collect();
    println!("{:?}", pares_cuadrados);
}

Adaptadores de iterador

Los adaptadores transforman un iterador en otro (son lazy):

fn main() {
    let palabras = vec!["hola", "mundo", "rust", "es", "genial"];
    
    // map: transformar cada elemento
    let longitudes: Vec<usize> = palabras.iter().map(|s| s.len()).collect();
    println!("{:?}", longitudes);
    
    // filter: conservar elementos que cumplen condición
    let largas: Vec<&&str> = palabras.iter().filter(|s| s.len() > 4).collect();
    println!("{:?}", largas);
    
    // enumerate: añadir índice
    for (i, palabra) in palabras.iter().enumerate() {
        println!("[{i}] {palabra}");
    }
    
    // zip: combinar dos iteradores en paralelo
    let numeros = [1, 2, 3, 4, 5];
    let pares: Vec<(&&str, &i32)> = palabras.iter().zip(numeros.iter()).collect();
    println!("{:?}", pares);
    
    // take y skip
    let primeras_dos: Vec<&&str> = palabras.iter().take(2).collect();
    let sin_primera: Vec<&&str> = palabras.iter().skip(1).collect();
    println!("Primeras 2: {:?}", primeras_dos);
    println!("Sin primera: {:?}", sin_primera);
    
    // chain: concatenar iteradores
    let a = [1, 2, 3];
    let b = [4, 5, 6];
    let concatenado: Vec<&i32> = a.iter().chain(b.iter()).collect();
    println!("{:?}", concatenado);
    
    // flat_map: mapear y aplanar
    let frases = vec!["hola mundo", "rust es genial"];
    let palabras_separadas: Vec<&str> = frases.iter()
        .flat_map(|frase| frase.split_whitespace())
        .collect();
    println!("{:?}", palabras_separadas);
    
    // windows y chunks
    let datos = [1, 2, 3, 4, 5, 6];
    let ventanas: Vec<&[i32]> = datos.windows(3).collect();
    println!("Ventanas de 3: {:?}", ventanas);
    
    let grupos: Vec<&[i32]> = datos.chunks(2).collect();
    println!("Grupos de 2: {:?}", grupos);
}

Consumidores de iterador

Los consumidores terminan la cadena y producen un resultado:

fn main() {
    let numeros: Vec<i32> = (1..=10).collect();
    
    // collect: construir una colección
    let cuadrados: Vec<i32> = numeros.iter().map(|&n| n * n).collect();
    
    // sum y product
    let suma: i32 = numeros.iter().sum();
    let producto: i64 = numeros.iter().map(|&n| n as i64).product();
    println!("Suma: {suma}, Producto: {producto}");
    
    // fold: acumulador personalizado
    let suma_manual = numeros.iter().fold(0_i32, |acc, &n| acc + n);
    let max_manual = numeros.iter().fold(i32::MIN, |acc, &n| acc.max(n));
    println!("Suma: {suma_manual}, Max: {max_manual}");
    
    // count, min, max
    let pares = numeros.iter().filter(|&&n| n % 2 == 0).count();
    println!("Pares: {pares}");
    println!("Min: {:?}", numeros.iter().min());
    println!("Max: {:?}", numeros.iter().max());
    
    // any y all
    let hay_par = numeros.iter().any(|&n| n % 2 == 0);
    let todos_positivos = numeros.iter().all(|&n| n > 0);
    println!("¿Hay par? {hay_par}, ¿Todos positivos? {todos_positivos}");
    
    // find y position
    let primer_par = numeros.iter().find(|&&n| n % 2 == 0);
    let posicion = numeros.iter().position(|&n| n == 5);
    println!("Primer par: {:?}, Posición de 5: {:?}", primer_par, posicion);
    
    // for_each: efecto lateral sin collect
    numeros.iter().filter(|&&n| n > 7).for_each(|n| print!("{n} "));
    println!();
}

Los closures e iteradores hacen que el código Rust sea conciso, expresivo y eficiente. En la próxima lección veremos concurrencia segura — una de las áreas donde Rust realmente brilla.

Los iteradores son lazy
Las operaciones como map, filter, take, skip no procesan nada hasta que llamas a un consumidor (collect, for_each, sum, fold, etc.). Esto significa que puedes construir cadenas muy largas sin overhead — el compilador las fusiona en un solo bucle.
iter() vs into_iter() vs iter_mut()
iter() presta elementos (&T), iter_mut() presta mutablamente (&mut T), into_iter() consume la colección (T). Para Vec<T>: .iter() da &T, .iter_mut() da &mut T, .into_iter() mueve T sacándolo del Vec. Cuando haces for x in &vec usas iter(), y for x in vec usas into_iter().
rust
fn main() {
    let numeros = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // Cadena de iteradores: sin copias intermedias
    let resultado: Vec<String> = numeros
        .iter()
        .filter(|&&n| n % 2 == 0)    // Solo pares: 2,4,6,8,10
        .map(|&n| n * n)             // Elevar al cuadrado: 4,16,36,64,100
        .filter(|&n| n > 20)         // Solo > 20: 36,64,100
        .enumerate()                  // Añadir índice: (0,36),(1,64),(2,100)
        .map(|(i, n)| format!("[{i}] {n}"))
        .collect();

    println!("{:?}", resultado);

    // fold: acumular un valor
    let suma: i32 = numeros.iter().fold(0, |acc, &n| acc + n);
    let producto: i64 = numeros.iter().map(|&n| n as i64).product();

    println!("Suma: {suma}, Producto: {producto}");

    // Closure que captura del entorno
    let umbral = 5;
    let mayores: Vec<i32> = numeros
        .iter()
        .filter(|&&n| n > umbral) // umbral capturado
        .cloned()
        .collect();
    println!("Mayores que {umbral}: {:?}", mayores);
}