Iteradores personalizados en Rust
En la página de iteradores usaste los iteradores que te da la biblioteca estándar: vec.iter(), .map(), .filter(), .sum(). Pero también puedes crear tus propios iteradores. Una cuenta atrás, los números de Fibonacci, las páginas de un informe, las líneas de un formato de archivo propio: cualquier cosa que entregue valores de uno en uno puede ser un iterador.
La buena noticia: solo escribes un método, next. A cambio, Rust te da todos los adaptadores (map, filter, take, sum, zip y decenas más) gratis. Piensa en una máquina de turnos. Tú construyes la parte que imprime el siguiente número. La fila, la pantalla y el cartel de “atendiendo al número…” funcionan solos.
Tu primer iterador: una cuenta atrás
Sección titulada «Tu primer iterador: una cuenta atrás»Para que un tipo sea un iterador, implementa el trait Iterator. Necesita dos cosas:
type Item: qué tipo de valor entrega el iterador (en traits avanzados tienes más sobre los tipos asociados).fn next(&mut self) -> Option<Self::Item>: devuelveSome(valor)con el siguiente valor, oNonecuando ya no quedan más.
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None // terminado } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let mut countdown = Countdown::new(3);
println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next()); println!("{:?}", countdown.next());
for n in Countdown::new(3) { println!("{}...", n); } println!("¡Despegue!");}Salida:
Some(3)Some(2)Some(1)NoneNone3...2...1...¡Despegue!Algunas cosas en las que fijarte:
nextrecibe&mut selfporque cambia el iterador. El struct recuerda por dónde va (current), y cada llamada lo hace avanzar. Por esocountdowntiene que serlet mut.- Devolver
Nonesignifica “ya terminé”. Un bucleforllama anextuna y otra vez y se detiene en el primerNone. - Nuestra cuenta atrás sigue devolviendo
Nonedespués de terminar. Es lo educado, pero el trait no lo exige: algunos iteradores podrían volver a empezar. Si necesitas esa garantía, llama a.fuse()sobre cualquier iterador.
Un iterador infinito: Fibonacci
Sección titulada «Un iterador infinito: Fibonacci»Un iterador no tiene por qué terminar. La sucesión de Fibonacci (0, 1, 1, 2, 3, 5, 8, …) sigue para siempre: cada número es la suma de los dos anteriores. El iterador solo necesita recordar los dos últimos números:
struct Fibonacci { current: u64, next: u64,}
impl Fibonacci { fn new() -> Self { Fibonacci { current: 0, next: 1 } }}
impl Iterator for Fibonacci { type Item = u64;
fn next(&mut self) -> Option<u64> { let value = self.current; // checked_add devuelve None si hay desbordamiento, y `?` pasa ese None hacia fuera, // así que el iterador termina en lugar de fallar cuando los números crecen demasiado. let new_next = self.current.checked_add(self.next)?; self.current = self.next; self.next = new_next; Some(value) }}
fn main() { let first_ten: Vec<u64> = Fibonacci::new().take(10).collect(); println!("{:?}", first_ten);}Salida:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]La clave es .take(10). Sin él, collect() intentaría recoger la sucesión entera. Con un iterador infinito, ponle siempre un límite con algo como take, take_while o find.
Todos los adaptadores, gratis
Sección titulada «Todos los adaptadores, gratis»Esta es la mejor parte. El trait Iterator tiene unos 75 métodos, y todos menos next tienen una implementación por defecto construida sobre next. Así que, en cuanto escribes next, tu tipo puede usarlos todos:
struct Countdown { current: u32,}
impl Countdown { fn new(from: u32) -> Self { Countdown { current: from } }}
impl Iterator for Countdown { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.current == 0 { None } else { let value = self.current; self.current -= 1; Some(value) } }}
fn main() { let total: u32 = Countdown::new(5).sum(); println!("Suma: {}", total);
let evens: Vec<u32> = Countdown::new(10).filter(|n| n % 2 == 0).collect(); println!("Pares: {:?}", evens);
let labels: Vec<String> = Countdown::new(3).map(|n| format!("T-{}", n)).collect(); println!("Etiquetas: {:?}", labels);
let multiple = Countdown::new(100).find(|n| n % 7 == 0); println!("Primer múltiplo de 7: {:?}", multiple);
let pairs: Vec<(u32, char)> = Countdown::new(3).zip(['a', 'b', 'c']).collect(); println!("Emparejados: {:?}", pairs);}Salida:
Suma: 15Pares: [10, 8, 6, 4, 2]Etiquetas: ["T-3", "T-2", "T-1"]Primer múltiplo de 7: Some(98)Emparejados: [(3, 'a'), (2, 'b'), (1, 'c')]Es un gran ejemplo de por qué los traits de Rust son tan útiles. Metes un método pequeño y sacas una caja de herramientas enorme.
Los iteradores son perezosos
Sección titulada «Los iteradores son perezosos»Los adaptadores como map y take no hacen nada por sí solos. Solo arman un “plan”. El trabajo ocurre únicamente cuando algo consume el iterador: un bucle for, collect, sum, etc. Vamos a demostrarlo con un iterador que imprime algo cada vez que se llama a next:
struct Noisy { n: u32,}
impl Iterator for Noisy { type Item = u32;
fn next(&mut self) -> Option<u32> { self.n += 1; println!(" se llamó a next(), devuelve {}", self.n); Some(self.n) }}
fn main() { println!("Armando la cadena..."); let chain = Noisy { n: 0 }.map(|x| x * 10).take(3); println!("Todavía no se ha ejecutado nada. Ahora a consumir:");
for value in chain { println!("recibido {}", value); } println!("Listo");}Salida:
Armando la cadena...Todavía no se ha ejecutado nada. Ahora a consumir: se llamó a next(), devuelve 1recibido 10 se llamó a next(), devuelve 2recibido 20 se llamó a next(), devuelve 3recibido 30ListoAquí hay dos lecciones. Primero, armar la cadena no imprimió nada: ninguna llamada a next(). Segundo, los valores pasan de uno en uno: un next, un map, un recibido, y luego el siguiente valor. No hay ninguna lista escondida en medio. Y Noisy nunca termina, pero el programa sí: después de tres elementos, take(3) simplemente deja de pedir.
Que tu colección funcione en bucles for
Sección titulada «Que tu colección funcione en bucles for»Hasta ahora, nuestros structs eran iteradores. Pero muchas veces tienes una colección (un struct que guarda elementos) y quieres que se pueda recorrer, igual que se recorre un Vec. Para eso implementas IntoIterator. Es el trait que usa un bucle for por detrás: for x in cosa en realidad llama a cosa.into_iter().
Las colecciones suelen ofrecer tres formas de recorrerlas, y ayuda verlas en tu propio tipo:
| Escribes | Método que hay detrás | Cada elemento es | La colección después |
|---|---|---|---|
for s in &playlist |
playlist.iter() |
&String (solo lectura) |
se puede seguir usando |
for s in &mut playlist |
playlist.iter_mut() |
&mut String (se puede cambiar) |
se puede seguir usando |
for s in playlist |
playlist.into_iter() |
String (con ownership) |
movida, ya no existe |
Aquí tienes una Playlist que admite las tres. Nuestro struct guarda sus canciones en un Vec, así que podemos entregar los iteradores del propio Vec en lugar de escribir otros nuevos:
struct Playlist { songs: Vec<String>,}
impl Playlist { fn iter(&self) -> std::slice::Iter<'_, String> { self.songs.iter() }
fn iter_mut(&mut self) -> std::slice::IterMut<'_, String> { self.songs.iter_mut() }}
// Hace que funcione `for song in &playlist`impl<'a> IntoIterator for &'a Playlist { type Item = &'a String; type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter() }}
// Hace que funcione `for song in &mut playlist`impl<'a> IntoIterator for &'a mut Playlist { type Item = &'a mut String; type IntoIter = std::slice::IterMut<'a, String>;
fn into_iter(self) -> Self::IntoIter { self.iter_mut() }}
// Hace que funcione `for song in playlist` (y consume la playlist)impl IntoIterator for Playlist { type Item = String; type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter { self.songs.into_iter() }}
fn main() { let mut playlist = Playlist { songs: vec![String::from("intro"), String::from("main theme"), String::from("outro")], };
for song in &playlist { println!("Leyendo: {}", song); }
for song in &mut playlist { *song = song.to_uppercase(); }
println!("La más larga: {:?}", playlist.iter().max_by_key(|s| s.len()));
for song in playlist { println!("Tomando: {}", song); } // El último bucle movió `playlist`, así que aquí ya no se puede usar.}Salida:
Leyendo: introLeyendo: main themeLeyendo: outroLa más larga: Some("MAIN THEME")Tomando: INTROTomando: MAIN THEMETomando: OUTROLos nombres de las canciones se quedan en inglés a propósito: max_by_key(|s| s.len()) compara longitudes en bytes, y las letras con tilde ocupan más de un byte, lo que podría cambiar cuál sale como “la más larga”.
Son muchas líneas, pero cada impl tiene la misma forma: dices qué es Item, dices qué tipo de iterador devuelves (IntoIter) y lo devuelves. El 'a es un lifetime (tiempo de vida): dice “los elementos prestados viven tanto como la playlist prestada”. Puedes copiar este patrón para casi cualquier colección que envuelva un Vec.
Ir hacia atrás y conocer la longitud
Sección titulada «Ir hacia atrás y conocer la longitud»Vale la pena conocer dos traits más. No los necesitarás a menudo, pero adaptadores como .rev() y .len() dependen de ellos:
DoubleEndedIteratorañadenext_back, que toma elementos desde el final. Desbloquea.rev().ExactSizeIteratorpromete que el iterador sabe exactamente cuántos elementos le quedan. Desbloquea.len(). Además, enIteratordas unsize_hintexacto.
// Produce low, low+1, ..., high-1 (como el rango low..high)struct Between { low: u32, high: u32,}
impl Iterator for Between { type Item = u32;
fn next(&mut self) -> Option<u32> { if self.low >= self.high { return None; } let value = self.low; self.low += 1; Some(value) }
fn size_hint(&self) -> (usize, Option<usize>) { let left = self.high.saturating_sub(self.low) as usize; (left, Some(left)) }}
impl DoubleEndedIterator for Between { fn next_back(&mut self) -> Option<u32> { if self.low >= self.high { return None; } self.high -= 1; Some(self.high) }}
impl ExactSizeIterator for Between {}
fn main() { let backwards: Vec<u32> = Between { low: 1, high: 6 }.rev().collect(); println!("{:?}", backwards);
let mut both_ends = Between { low: 1, high: 6 }; println!("delante {:?}, detrás {:?}", both_ends.next(), both_ends.next_back()); println!("quedan {}", both_ends.len());}Salida:
[5, 4, 3, 2, 1]delante Some(1), detrás Some(5)quedan 3Los dos extremos avanzan el uno hacia el otro, y el iterador termina cuando se encuentran. impl ExactSizeIterator for Between {} está vacío: len() usa el size_hint que ya escribiste.
Atajos: from_fn y successors
Sección titulada «Atajos: from_fn y successors»Escribir un struct y un impl es ideal para iteradores que vas a reutilizar. Para uno rápido y de un solo uso, la biblioteca estándar tiene dos ayudantes en std::iter:
iter::from_fn(closure): llama a tu closure cada vez que hace falta un valor. El closure devuelveSome(valor)oNone, igual quenext.iter::successors(first, closure): empieza con un primer valor, y el closure calcula cada valor siguiente a partir del anterior. DevolverNonelo termina.
use std::iter;
fn main() { // from_fn: el closure guarda su propio estado (count) let mut count = 3; let countdown: Vec<u32> = iter::from_fn(|| { if count == 0 { None } else { count -= 1; Some(count + 1) } }) .collect(); println!("{:?}", countdown);
// successors: cada valor se calcula a partir del anterior let powers: Vec<u32> = iter::successors(Some(1u32), |&n| n.checked_mul(10)).collect(); println!("{:?}", powers);
// Fibonacci en una línea: lleva los dos últimos números como una pareja let fib: Vec<u64> = iter::successors(Some((0u64, 1u64)), |&(a, b)| Some((b, a + b))) .map(|(a, _)| a) .take(10) .collect(); println!("{:?}", fib);}Salida:
[3, 2, 1][1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000][0, 1, 1, 2, 3, 5, 8, 13, 21, 34]powers se detiene solo: la siguiente potencia de diez, 10.000.000.000, no cabe en un u32, así que checked_mul devuelve None y el iterador termina. Una buena regla: usa estos atajos dentro de una sola función, y escribe un struct con nombre cuando otro código necesite crear el iterador o cuando tenga mucho estado.
Errores comunes
Sección titulada «Errores comunes»Olvidar mut al llamar tú mismo a next. next cambia el iterador, así que la variable tiene que ser mutable:
let countdown = Countdown { current: 3 };println!("{:?}", countdown.next());error[E0596]: cannot borrow `countdown` as mutable, as it is not declared as mutable --> src/main.rs:20:22 |20 | println!("{:?}", countdown.next()); | ^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |19 | let mut countdown = Countdown { current: 3 }; | +++El compilador dice que no puede tomar countdown prestada como mutable porque no se declaró como mutable. Añade mut. (Un bucle for no lo necesita: toma el iterador y se encarga de eso por ti).
Olvidar type Item. El trait necesita saber qué entregas:
error[E0046]: not all trait items implemented, missing: `Item` --> src/main.rs:5:1 |5 | impl Iterator for Countdown { | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation | = help: implement the missing item: `type Item = /* Type */;`El mensaje dice que falta el elemento Item en la implementación. Añade type Item = u32; (o lo que produzca tu iterador) dentro del impl.
Recoger un iterador infinito. Fibonacci::new().collect::<Vec<_>>() (sin take) compila sin problemas, pero nunca termina de una forma útil: se ejecuta hasta que los números se desbordan, o para siempre si tu next nunca devuelve None. El compilador no puede detectarlo. Cuando un iterador pueda ser infinito, añade siempre take, take_while o find antes de consumirlo.
Resumen
Sección titulada «Resumen»- Implementa
Iteratorcontype Itemyfn next(&mut self) -> Option<Self::Item>. DevuelveNonecuando termines. - En cuanto existe
next, obtienes gratismap,filter,take,sum,zipy decenas más. - Los iteradores son perezosos (lazy): no pasa nada hasta que un bucle
for,collect,sum, etc. los consume, y los elementos pasan de uno en uno. - Implementa
IntoIteratorpara&MiTipo,&mut MiTipoyMiTipopara admitir buclesforque toman prestado, toman prestado de forma mutable o consumen. DoubleEndedIteratorhabilita.rev();ExactSizeIteratorhabilita.len().- Para iteradores rápidos de un solo uso, usa
std::iter::from_fnostd::iter::successors.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué debería devolver next cuando al iterador ya no le quedan elementos?
2. Si solo implementas next, ¿puedes llamar a .map() y .sum() sobre tu iterador?
3. En el ejemplo de evaluación perezosa, ¿cuándo se llama a next() por primera vez?
4. ¿Qué impl hace que funcione for song in &playlist?
5. ¿Qué trait necesitas para que .rev() funcione con tu iterador?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- Procesar una serie de elementos con iteradores — El libro de Rust (en inglés): cómo funcionan
Iteratorynext, y por qué los iteradores son perezosos. - Módulo
std::iter(en inglés): la guía oficial de los iteradores, con una sección sobre cómo implementar los tuyos y sobreIntoIterator. - El trait
Iterator— documentación de la biblioteca estándar (en inglés): todos los adaptadores que obtienes gratis en cuanto escribesnext. - Iterators — Rust by Example (en inglés): un ejemplo corto de un iterador de Fibonacci que puedes ejecutar y modificar.
std::iter::successors(en inglés): la documentación para construir un iterador a partir de un valor inicial y una regla para el “siguiente”.