Lección 22 de 26 Rust funcional
Iteradores
- Un iterador entrega los valores uno a uno. v.iter() va poniendo los números de v en la cinta.
- filter es un adaptador: deja pasar solo los que cumplen la condición. Aquí, los pares: 2, 4 y 6.
- map transforma cada valor con una closure: n * 10 convierte el 2, el 4 y el 6 en 20, 40 y 60.
- sum es un consumidor: pone en marcha la cadena y la resume en 120. Sin él, el iterador no hace nada.
Un iterador produce una secuencia de valores, uno detrás de otro. Ya los has usado sin darte cuenta: cada for de Rust funciona con un iterador por debajo.
Lo interesante es todo lo que puedes hacer con ellos: transformar, filtrar, combinar y resumir colecciones encadenando métodos, sin índices ni variables auxiliares. Junto con las closures de la lección anterior, son la forma más idiomática de trabajar con datos en Rust.
El trait Iterator
Un iterador es cualquier tipo que implemente el trait Iterator. Solo exige un método, next, que devuelve el siguiente elemento envuelto en Some, o None cuando ya no quedan:
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
// ...y decenas de métodos que ya vienen implementados
}type Item es el tipo de los elementos que produce. Puedes llamar a next a mano para verlo en acción:
fn main() {
let numeros = vec![10, 20, 30];
let mut it = numeros.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
}El iterador es mut porque next cambia su estado interno: recuerda por dónde va.
iter, iter_mut e into_iter
Una colección se puede recorrer de tres maneras, igual que se puede usar un valor de tres maneras:
| Método | Produce | La colección… |
|---|---|---|
iter() | &T | Se presta para leer |
iter_mut() | &mut T | Se presta para modificar |
into_iter() | T | Se consume: ya no podrás usarla |
fn main() {
let mut notas = vec![5, 7, 9];
for nota in notas.iter_mut() {
*nota += 1;
}
println!("{notas:?}");
let total: i32 = notas.into_iter().sum();
println!("Total: {total}");
// notas ya no existe: into_iter se la ha quedado
}El for hace esto por ti: for x in &v usa v.iter(), for x in &mut v usa v.iter_mut() y for x in v usa v.into_iter().
Los iteradores son perezosos
Crear un iterador o encadenarle transformaciones no hace nada hasta que algo lo consume:
fn main() {
let numeros = vec![1, 2, 3];
let dobles = numeros.iter().map(|x| {
println!("procesando {x}");
x * 2
});
println!("Todavía no se ha procesado nada");
let resultado: Vec<i32> = dobles.collect();
println!("{resultado:?}");
}Si escribes un map y no lo consumes, el compilador te avisa: iterators are lazy and do nothing unless consumed.
Adaptadores: transformar el iterador
Los adaptadores reciben un iterador y devuelven otro. Se pueden encadenar tantos como quieras:
| Adaptador | Qué hace |
|---|---|
map(f) | Transforma cada elemento con la closure f |
filter(f) | Deja pasar solo los elementos para los que f devuelve true |
enumerate() | Añade el índice: produce (0, a), (1, b)… |
zip(otro) | Empareja con otro iterador: (a1, b1), (a2, b2)… |
rev() | Recorre al revés |
take(n) / skip(n) | Se queda con los n primeros / se los salta |
chain(otro) | Pone otro iterador a continuación |
fn main() {
let numeros = [1, 2, 3, 4, 5, 6];
let cuadrados: Vec<i32> = numeros.iter().map(|n| n * n).collect();
let pares: Vec<i32> = numeros.into_iter().filter(|n| n % 2 == 0).collect();
let ultimos: Vec<i32> = numeros.into_iter().rev().take(2).collect();
let resto: Vec<i32> = numeros.into_iter().skip(4).collect();
println!("{cuadrados:?}");
println!("{pares:?}");
println!("{ultimos:?}");
println!("{resto:?}");
}Referencias: El adaptador
filterle pasa a la closure una referencia al elemento, para no quitárselo. Por eso a veces verás*no patrones como|&n|. Si el compilador se queja de tipos como&&i32, es por esto.
enumerate, zip y chain sirven para combinar:
fn main() {
let nombres = ["Ana", "Luis", "Marta"];
let edades = [28, 35, 22];
for (i, nombre) in nombres.iter().enumerate() {
println!("{i}: {nombre}");
}
for (nombre, edad) in nombres.iter().zip(edades.iter()) {
println!("{nombre} tiene {edad} años");
}
let todos: Vec<i32> = [1, 2].into_iter().chain([3, 4]).collect();
println!("{todos:?}");
}Consumidores: obtener un resultado
Los consumidores recorren el iterador y producen un valor final. Son los que ponen en marcha toda la cadena:
fn main() {
let numeros = vec![4, 8, 15, 16, 23, 42];
let suma: i32 = numeros.iter().sum();
let mayores = numeros.iter().filter(|n| **n > 10).count();
let maximo = numeros.iter().max();
let hay_impares = numeros.iter().any(|n| n % 2 == 1);
let todos_positivos = numeros.iter().all(|n| *n > 0);
let primero_grande = numeros.iter().find(|n| **n > 20);
let posicion = numeros.iter().position(|n| *n == 16);
println!("suma = {suma}, mayores de 10 = {mayores}");
println!("máximo = {maximo:?}");
println!("¿impares? {hay_impares}, ¿positivos? {todos_positivos}");
println!("primero > 20 = {primero_grande:?}, posición del 16 = {posicion:?}");
}fold es el consumidor más general: parte de un valor inicial y lo va actualizando con cada elemento. sum o count son casos particulares de fold:
fn main() {
let digitos = [1, 9, 8, 4];
let numero = digitos.iter().fold(0, |acumulado, d| acumulado * 10 + d);
println!("{numero}");
}collect: de vuelta a una colección
collect construye una colección a partir del iterador. Como puede crear muchas distintas, tienes que indicar cuál con el tipo de la variable:
use std::collections::HashMap;
fn main() {
let palabra: String = ['h', 'o', 'l', 'a'].iter().collect();
let gritando: String = "rust".chars().map(|c| c.to_ascii_uppercase()).collect();
let precios: HashMap<&str, u32> = [("pan", 1), ("leche", 2)].into_iter().collect();
println!("{palabra} {gritando}");
println!("El pan cuesta {} €", precios["pan"]);
}Para un HashMap basta con que el iterador produzca tuplas (clave, valor).
Encadenar todo
La potencia de verdad aparece al combinar adaptadores y consumidores en una sola cadena. Así se procesa v.iter().filter(|n| *n % 2 == 0).map(|n| n * 10).sum():
Cada elemento recorre la cadena entera antes de que empiece el siguiente, así que no se crean vectores intermedios. Un ejemplo más realista:
fn main() {
let productos = vec![("teclado", 45), ("monitor", 180), ("cable", 5), ("silla", 120)];
let caros: Vec<String> = productos
.iter()
.filter(|(_, precio)| *precio >= 50)
.map(|(nombre, precio)| format!("{nombre}: {precio} €"))
.collect();
let total: i32 = productos.iter().map(|(_, precio)| precio).sum();
println!("{caros:?}");
println!("Total: {total} €");
}Compáralo con la versión con bucle: hace lo mismo, pero hay que leerla entera para saber qué hace.
let mut caros = Vec::new();
for (nombre, precio) in &productos {
if *precio >= 50 {
caros.push(format!("{nombre}: {precio} €"));
}
}Coste cero: Los iteradores son una abstracción de coste cero. El compilador los convierte en el mismo código máquina que el bucle escrito a mano, y a veces en uno más rápido porque puede ahorrarse comprobaciones de límites. Elige la versión que se lea mejor.
Tu propio iterador
Para que un tipo tuyo sea un iterador, implementa Iterator con su next. A cambio, recibes gratis todos los adaptadores y consumidores:
struct Contador {
actual: u32,
limite: u32,
}
impl Contador {
fn new(limite: u32) -> Self {
Contador { actual: 0, limite }
}
}
impl Iterator for Contador {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.actual < self.limite {
self.actual += 1;
Some(self.actual)
} else {
None
}
}
}
fn main() {
let total: u32 = Contador::new(5).sum();
println!("Suma de 1 a 5: {total}");
let pares: Vec<u32> = Contador::new(10).filter(|n| n % 2 == 0).collect();
println!("{pares:?}");
}Hora de practicar
Ejercicios
0/5 resueltos
Reto 1 de 5 Resuelto
iter(), map y collect. No uses for.[1, 4, 9, 16, 25]
Al ejecutar, unos tests ocultos comprueban tus funciones.
Pulsa Ejecutar para compilar y ver aquí la salida de tu programa.