Closures e Iteradores
Los closures vienen en tres sabores —Fn, FnMut, FnOnce— y las cadenas de iteradores se mantienen perezosas hasta que se consumen, pero compilan tan rápido como un bucle escrito a mano.
Un closure es la función anónima de Rust — pero a diferencia de un fn normal, un closure puede capturar variables del entorno donde se define y llevarlas consigo a donde vaya. La sintaxis se apoya en la inferencia: |x| x + 1 no necesita anotaciones de tipo porque el compilador deduce el tipo de x (y el de retorno) a partir de cómo se usa el closure la primera vez, del mismo modo que infiere el tipo de un let. Esto hace que los closures sean ideales para las dos cosas para las que los usarás constantemente en Rust: pasar un fragmento pequeño de lógica a otra función (un comparador, un callback, un predicado) y construir las canalizaciones sobre las que corren los métodos de Iterator.
Esa inferencia trae una arruga que conviene conocer de entrada: una vez que se infieren los tipos de parámetro y retorno de un closure a partir de su primera llamada, quedan fijos — no puedes llamar al mismo closure con un i32 la primera vez y un f64 la segunda, como sí podrías con una función genérica. Si necesitas ese tipo de flexibilidad, quieres una función genérica con un límite de trait en su lugar; los tipos de un closure, una vez inferidos, son tan concretos como si los hubieras escrito a mano.
La forma en que un closure captura su entorno determina cuál de tres traits implementa, y Rust elige el menos restrictivo que pueda usar según lo que el cuerpo del closure realmente hace. Todo closure implementa FnOnce, porque todo closure puede llamarse al menos una vez. Si el cuerpo no mueve hacia afuera ningún valor capturado (solo lee o muta a través de una referencia), el closure también implementa FnMut, lo que significa que puede llamarse repetidamente, mutando sus capturas en el camino. Si el cuerpo tampoco necesita mutar nada —solo lee—, el closure además implementa Fn, y puede llamarse cualquier cantidad de veces, incluso de forma concurrente desde varios lugares. Estos no son tres traits inconexos entre los que elegir: forman una jerarquía, y un parámetro de función limitado por Fn aceptará sin problema closures que también implementen FnMut y FnOnce, pero un parámetro limitado por FnOnce solo aceptará los que sean, como mucho, igual de permisivos.
Por defecto, un closure captura lo mínimo que necesita, y por referencia cuando puede permitírselo — lo cual es eficiente, pero a veces no es lo que quieres, sobre todo en cuanto entran hilos en juego. La palabra clave move obliga a un closure a tomar posesión de todo lo que captura, incluso de variables que de otro modo solo habría tomado prestadas. Esto importa sobre todo con thread::spawn, cuyo closure podría ejecutarse después de que la función actual ya haya retornado, así que no se le puede permitir mantener un préstamo hacia un stack frame que quizá ya no exista — move es lo que hace seguro entregarle datos a un hilo nuevo.
Iterator es un trait, y sorprendentemente pequeño: implementarlo solo exige un único método, fn next(&mut self) -> Option<Self::Item>, que devuelve Some(item) hasta que la secuencia se agota y None después. Todo lo demás —map, filter, fold, collect, y decenas más— se ofrece como métodos por defecto construidos encima de next, el mismo mecanismo de métodos por defecto de la lección de traits, solo que usado a una escala mucho mayor. Y lo más importante: los iteradores son perezosos (lazy). Llamar a .map(...) sobre un iterador no recorre nada ni calcula ningún valor; solo envuelve el iterador original en uno nuevo que aplicará el closure cuando —y solo cuando— algo finalmente le pida su siguiente elemento.
Esa pereza es lo que hace que las cadenas de iteradores se compongan con tanta limpieza. numbers.iter().map(|n| n * n).filter(|n| n % 2 == 0).fold(0, |acc, n| acc + n) se lee como una canalización —eleva al cuadrado, luego quédate con los pares, luego suma— y nada se ejecuta de verdad hasta que fold empieza a extraer elementos de la cadena uno a uno, aplicando los tres pasos a cada elemento antes de pasar al siguiente, en lugar de materializar un vector intermedio después de cada etapa. collect es el adaptador al que recurrirás más a menudo para terminar una cadena, porque puede construir prácticamente cualquier colección que el compilador pueda inferir por contexto —un Vec, un HashMap, un String— con solo anotar el tipo de la variable.
Es razonable esperar que toda esta abstracción cueste algo en tiempo de ejecución —llamadas a función extra, structs envoltorio, indirección— pero, en gran medida, no es así. Este es el principio de 'abstracciones de costo cero' de Rust en acción: como el tipo de cada adaptador se conoce en tiempo de compilación y cada closure es un tipo concreto e inlineable (no un valor dinámico en el heap salvo que lo pidas explícitamente), el optimizador puede ver a través de toda la cadena y generar código que, tras la optimización, resulta esencialmente idéntico al bucle for escrito a mano, con un if y un total acumulado, que habrías escrito en su lugar. Consigues escribir la versión legible y declarativa y aun así obtener el ensamblador de la versión imperativa.
fn apply_to_five<F>(f: F) -> i32whereF: Fn(i32) -> i32,{f(5)}fn main() {let offset = 3;// This closure only reads `offset`, so it implements `Fn`// (and therefore also `FnMut` and `FnOnce`).let add_offset = |x| x + offset;println!("{}", apply_to_five(add_offset));// The same closure can be called again: it never consumed `offset`.println!("{}", add_offset(10));}
A closure that only reads its captured variable implements Fn, so it can be passed into a generic function and then called again directly afterward.
use std::thread;fn main() {let data = vec![1, 2, 3];// `move` forces the closure to take ownership of `data` instead of// borrowing it, which is required here: the spawned thread might// outlive the current scope, so it can't hold a borrow into it.let handle = thread::spawn(move || {println!("data from thread: {:?}", data);});handle.join().unwrap();}
The move keyword forces the closure passed to thread::spawn to take ownership of data, which is required because the spawned thread could outlive the current stack frame.
fn main() {let numbers = vec![1, 2, 3, 4, 5, 6];// Nothing runs yet: `map` and `filter` just build up a lazy pipeline.let pipeline = numbers.iter().map(|n| n * n).filter(|n| n % 2 == 0);// The pipeline only executes once something consumes it, like `fold`.let sum_of_even_squares = pipeline.fold(0, |acc, n| acc + n);println!("sum of even squares: {}", sum_of_even_squares);// `collect` is the adaptor you'll reach for most often to end a chain.let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();println!("{:?}", doubled);}
An iterator pipeline that does nothing until fold consumes it, illustrating both laziness and the map/filter/fold/collect adaptors.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. You want to pass a closure to a function that calls it repeatedly in a loop. The closure captures a Vec<i32> from its environment and calls .push() on it each time it runs. Which trait bound must the function require, and why?