Iteradores y closures en Rust

1. Descripción general

Closures e iteradores son dos herramientas que te permiten procesar datos de forma funcional en Rust manteniendo un rendimiento equivalente a los bucles escritos manualmente. Este artículo explica cómo los closures capturan variables del entorno y cubre los tres traits Fn, FnMut, FnOnce. Luego veremos el trait Iterator, los adaptadores más comunes y cómo escribir tu propio iterador. Después de leerlo, escribirás pipelines como filter → map → collect con confianza. También entenderás por qué el compilador a veces rechaza tu código.

Todos los ejemplos usan Rust 1.80+ (edición 2021), solo requieren la librería estándar.

2. Closures básicos

2.1. Sintaxis

Un closure es una función anónima que puede “ver” variables en el ámbito que la rodea:

fn main() {
    let discount = 10; // porcentaje
    let apply_discount = |price: u32| price - price * discount / 100;

    println!("{}", apply_discount(250));
    println!("{}", apply_discount(1000));
}
225
900

Los parámetros van entre dos barras verticales |...|, el cuerpo del closure viene justo después. El closure usa la variable discount aunque no se le pasa como parámetro. Una función fn normal no puede hacer esto.

Los tipos de parámetro y el tipo de retorno generalmente se infieren automáticamente. Puedes escribir |price| ... si el compilador tiene suficiente información. Sin embargo, el tipo solo se infiere una vez. Si la primera llamada es con u32 y la segunda con i64, obtendrás un error de compilación.

2.2. Tres formas de capturar variables

Un closure captura variables de la forma “más ligera” que su cuerpo necesita. El orden de preferencia es: borrow inmutable, borrow mutable, y finalmente tomar ownership.

fn main() {
    let mut cart = vec!["bolígrafo"];

    let print_cart = || println!("Carrito: {:?}", cart); // borrow &cart
    print_cart();

    let mut add_item = |item| cart.push(item);        // borrow &mut cart
    add_item("cuaderno");
    add_item("regla");

    println!("Final: {:?}", cart);
}
Carrito: ["bolígrafo"]
Final: ["bolígrafo", "cuaderno", "regla"]

Analicemos cada closure:

  • print_cart solo lee cart, por lo que captura un &cart.
  • add_item llama a push, así que necesita &mut cart. La variable del closure también debe declararse mut, porque llamarla modifica su estado interno.
  • El código compila gracias a los lifetimes no léxicos. print_cart no se usa después de su primera llamada, así que el borrow inmutable termina antes de que comience el borrow mutable.

Cuando necesitas que el closure sea propietario de los datos, agregas la palabra clave move. El caso típico es pasar el closure a otro thread:

use std::thread;

fn main() {
    let customer = String::from("Lan");

    let handle = thread::spawn(move || {
        println!("Procesando pedido de {customer}");
    });

    handle.join().unwrap();
}
Procesando pedido de Lan

Sin move, el compilador reportaría un error. El nuevo thread puede vivir más que main, así que una referencia a customer no sería segura. Con move, el String se transfiere completamente al closure. Después de esa línea, no puedes usar customer en main.

3. Los tres traits Fn, FnMut y FnOnce

Cada closure automáticamente implementa uno o varios de estos tres traits. Cuál implementa depende de qué hace el closure con las variables capturadas:

Trait Lo que el closure hace con variables capturadas Puede llamarse
Fn Solo lectura Varias veces
FnMut Modifica Varias veces
FnOnce Consume (move hacia afuera) Una vez

Estos tres traits tienen una relación de herencia: todo Fn es también FnMut, y todo FnMut es también FnOnce. Al escribir una función que acepta un closure, deberías elegir el trait más flexible que necesites. De esta forma, quien la llama tiene más opciones.

fn run_twice<F: Fn()>(f: F) {
    f();
    f();
}

fn run_twice_mut<F: FnMut()>(mut f: F) {
    f();
    f();
}

fn run_once<F: FnOnce() -> String>(f: F) -> String {
    f()
}

fn main() {
    let name = String::from("Minh");
    run_twice(|| println!("Hola {name}"));

    let mut count = 0;
    run_twice_mut(|| count += 1);
    println!("count = {count}");

    let receipt = run_once(move || name + " - pagado");
    println!("{receipt}");
}
Hola Minh
Hola Minh
count = 2
Minh - pagado

Las tres llamadas corresponden a los tres traits:

  • El primer closure solo lee name, así que es Fn.
  • El segundo closure incrementa count, así que es FnMut. Pasarlo a run_twice causaría un error.
  • El último closure usa name + "...". El operador + en String toma ownership de name, así que el closure solo se puede llamar una vez e implementa solo FnOnce.

3.1. Devolver un closure desde una función

Cada closure tiene su propio tipo anónimo único. Por eso no puedes escribir el nombre de ese tipo. Hay dos formas de devolver un closure:

fn make_discount(percent: u32) -> impl Fn(u32) -> u32 {
    move |price| price - price * percent / 100
}

fn pick_rule(is_vip: bool) -> Box<dyn Fn(u32) -> u32> {
    if is_vip {
        Box::new(|price| price * 80 / 100)
    } else {
        Box::new(|price| price)
    }
}

fn main() {
    let summer_sale = make_discount(15);
    println!("{}", summer_sale(200));

    let rule = pick_rule(true);
    println!("{}", rule(500));
}
170
400
  • impl Fn(...) se usa cuando la función siempre devuelve un tipo de closure específico. Este enfoque no asigna en el heap y no tiene costo de llamada dinámica.
  • Box<dyn Fn(...)> se usa cuando diferentes ramas devuelven closures distintos. Los dos closures en pick_rule tienen tipos diferentes, así que deben unificarse bajo un trait object.
  • move en make_discount es obligatorio. Sin él, el closure tomaría prestado percent, que desaparece al final de la función.

4. El trait Iterator

4.1. El núcleo: la función next

Todo iterador en Rust gira en torno a un trait muy pequeño:

pub trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... docenas de métodos proporcionados como map, filter, sum ...
}

next devuelve Some(elemento) hasta que se acaban los datos, luego devuelve None. Un bucle for es esencialmente sintaxis para llamar a next repetidamente.

4.2. iter, iter_mut e into_iter

Una colección puede proporcionar tres tipos diferentes de iteradores:

fn main() {
    let mut prices = vec![100, 200, 300];

    for p in prices.iter() {          // p: &i32
        print!("{p} ");
    }
    println!();

    for p in prices.iter_mut() {      // p: &mut i32
        *p += 10;
    }

    for p in prices.into_iter() {     // p: i32, vector es movido
        print!("{p} ");
    }
    println!();
    // println!("{:?}", prices); // error: valor usado después de move
}
100 200 300 
110 210 310 
  • iter() proporciona referencias inmutables. La colección aún se puede usar después.
  • iter_mut() proporciona referencias mutables para modificar elementos en el lugar.
  • into_iter() toma ownership. Escribir for p in prices también llama a into_iter() implícitamente.

5. Adaptadores y consumidores

Los métodos en un iterador se dividen en dos grupos:

  • Adaptadores (map, filter, take, zip, enumerate…) transforman este iterador en otro. Son lazy: no ejecutan nada hasta que alguien los consume.
  • Consumidores (collect, sum, count, fold, for_each, find…) realmente llaman a next y producen un resultado.

Usaremos un struct Order como datos de ejemplo:

#[derive(Debug)]
pub struct Order {
    pub id: u32,
    pub customer: String,
    pub total: u32,
    pub paid: bool,
}

pub fn unpaid_total(orders: &[Order]) -> u32 {
    orders.iter()
        .filter(|o| !o.paid)
        .map(|o| o.total)
        .sum()
}

pub fn big_customers(orders: &[Order], threshold: u32) -> Vec<&str> {
    orders.iter()
        .filter(|o| o.total >= threshold)
        .map(|o| o.customer.as_str())
        .collect()
}

fn main() {
    let orders = vec![
        Order { id: 1, customer: "An".into(),   total: 150_000, paid: true },
        Order { id: 2, customer: "Bình".into(), total: 80_000,  paid: false },
        Order { id: 3, customer: "Chi".into(),  total: 320_000, paid: false },
    ];

    println!("Sin cobrar: {}", unpaid_total(&orders));
    println!("Clientes grandes: {:?}", big_customers(&orders, 100_000));

    let summary = orders.iter()
        .fold(String::new(), |acc, o| acc + &format!("#{} ", o.id));
    println!("Pedidos: {summary}");
}
Sin cobrar: 400000
Clientes grandes: ["An", "Chi"]
Pedidos: #1 #2 #3 

Los pasos en el ejemplo:

  • filter(|o| !o.paid) acepta un closure que devuelve bool. El parámetro o tiene tipo &&Order, porque filter pasa una referencia al elemento, y el elemento de iter() ya es &Order. La desreferencia automática te permite escribir o.paid normalmente.
  • map(|o| o.total) transforma cada pedido en un número. sum() suma todo.
  • collect() reúne los resultados en una colección. El tipo destino se toma del tipo de retorno Vec<&str> de la función.
  • fold acepta un valor inicial y un closure (acumulador, elemento) -> acumulador_nuevo. Este es el consumidor más general.

5.1. Evaluación lazy

El siguiente código muestra que los adaptadores solo se ejecutan cuando se consumen:

fn main() {
    let ids = vec![1, 2, 3];
    let doubled = ids.iter().map(|x| {
        println!("map {x}");
        x * 2
    });

    println!("Aún no hay nada ejecutándose");
    let result: Vec<i32> = doubled.collect();
    println!("{result:?}");
}
Aún no hay nada ejecutándose
map 1
map 2
map 3
[2, 4, 6]

Gracias a la evaluación lazy, una cadena filter → map → sum solo recorre los datos una vez sin crear colecciones intermedias. El compilador generalmente optimiza este pipeline en código máquina equivalente a un bucle escrito manualmente. Esta es la razón por la que los iteradores se llaman “abstracción de costo cero”.

6. Escribe tu propio iterador

Solo necesitas implementar next. Todos los adaptadores restantes vienen gratis. El siguiente ejemplo genera números de factura en el formato FC-0001:

pub struct InvoiceNumbers {
    current: u32,
    end: u32,
}

impl InvoiceNumbers {
    pub fn new(start: u32, end: u32) -> Self {
        Self { current: start, end }
    }
}

impl Iterator for InvoiceNumbers {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current > self.end {
            return None;
        }
        let number = format!("FC-{:04}", self.current);
        self.current += 1;
        Some(number)
    }
}

fn main() {
    let customers = ["An", "Bình", "Chi"];

    for (invoice, customer) in InvoiceNumbers::new(7, 100).zip(customers) {
        println!("{invoice} -> {customer}");
    }
}
FC-0007 -> An
FC-0008 -> Bình
FC-0009 -> Chi

Explicación:

  • type Item = String declara el tipo de elemento que devuelve el iterador.
  • next actualiza el estado mediante &mut self y devuelve None cuando supera end.
  • zip empareja dos iteradores en tuplas y se detiene cuando el más corto se agota. Por eso solo se imprimen tres líneas, aunque el rango va hasta 100.

7. Escribir pruebas

Rust tiene un framework de pruebas incorporado. Colocas un módulo de prueba en el mismo archivo que contiene unpaid_total e InvoiceNumbers:

#[cfg(test)]
mod tests {
    use super::*;

    fn order(id: u32, total: u32, paid: bool) -> Order {
        Order { id, customer: format!("KH{id}"), total, paid }
    }

    #[test]
    fn unpaid_total_only_counts_unpaid_orders() {
        let orders = vec![order(1, 100, true), order(2, 50, false), order(3, 70, false)];
        assert_eq!(unpaid_total(&orders), 120);
    }

    #[test]
    fn unpaid_total_of_empty_slice_is_zero() {
        assert_eq!(unpaid_total(&[]), 0);
    }

    #[test]
    fn invoice_numbers_stop_after_end() {
        let numbers: Vec<String> = InvoiceNumbers::new(9, 11).collect();
        assert_eq!(numbers, vec!["FC-0009", "FC-0010", "FC-0011"]);
    }
}
cargo test
running 3 tests
test tests::invoice_numbers_stop_after_end ... ok
test tests::unpaid_total_of_empty_slice_is_zero ... ok
test tests::unpaid_total_only_counts_unpaid_orders ... ok

test result: ok. 3 passed; 0 failed; 0 ignored

La segunda prueba verifica un caso límite. sum() en un iterador vacío devuelve 0, no hace panic.

8. Errores comunes y cuándo no usar esto

Nota: Olvidar un consumidor es el error más común. La línea orders.iter().map(|o| println!("{}", o.id)); no imprime nada. El compilador advierte “iterators are lazy and do nothing unless consumed”. Cuando solo necesites efectos secundarios, usa un bucle for o for_each.

Algunos otros errores que encontrarás:

  • collect no conoce el tipo destino. Escribir let v = it.collect(); causará un error “type annotations needed”. Necesitas especificar let v: Vec<_> = ... o usar turbofish collect::<Vec<_>>().
  • Usar into_iter() y luego querer reutilizar la colección. Si solo necesitas leer, usa iter().
  • El closure captura un borrow demasiado tiempo. Un closure FnMut capturando &mut cart te impide leer cart hasta la última vez que uses el closure. Limita el alcance del closure o llámalo y luego lee.
  • Necesitar ? dentro de map. El operador ? en un closure solo devuelve del closure, no de la función envolvente. Usa collect::<Result<Vec<_>, _>>(). Esto se detiene en el primer error y devuelve Err.

Cuándo no deberías forzar todo en una cadena de iteradores:

  • La lógica tiene muchas ramas, muchos puntos break/continue, o actualiza varias variables simultáneamente. Un for simple es más legible que un fold de diez líneas.
  • Necesitas devolver closures de muchas ramas pero te preocupa el rendimiento. Box<dyn Fn> tiene costo de asignación y llamada dinámica. Considera usar enum o genéricos en su lugar.

9. Conclusión

Los closures en Rust capturan variables de la forma más ligera posible: borrow, borrow mutable, o move. De esto, los closures implementan automáticamente Fn, FnMut o FnOnce. Al escribir una función que acepta un closure, elige el trait más flexible que necesites.

Un iterador se basa en un solo método next. Adaptadores como map, filter, zip son lazy, mientras que consumidores como collect, sum, fold realmente ejecutan el pipeline. Combinando estas dos herramientas, escribes código conciso para procesar datos, seguro en memoria y tan rápido como bucles escritos manualmente. También deberías saber cuándo un simple for es la mejor opción.

rustiteradorclosureprogramación funcional