Escribe tu propio iterador: impl Iterator con next
Todo el ecosistema de iteradores descansa en un trait con un solo método obligatorio. Implementa type Item y next para tu propio tipo y hereda gratis los setenta adaptadores y consumidores. La diferencia entre Iterator e IntoIterator.
Hasta ahora has consumido iteradores; ahora vas a fabricarlos. Y aquí aguarda una de las lecciones de diseño más limpias de Rust: para que tu propio tipo se integre en todo el universo de map, filter, zip, collect y compañía, solo debes implementar un método. Iterator es un trait con un único requisito obligatorio, next, y todo lo demás son cortesías por defecto construidas encima. Escribe seis líneas y tu tipo hablará el idioma de toda la biblioteca estándar.
- Implementar
Iteratorpara un tipo propio definiendotype Itemynext. - Entender que
nextdevuelveOption<Self::Item>:Somemientras haya,Noneal agotarse. - Heredar gratis los adaptadores y consumidores por defecto del trait.
- Distinguir
IteratordeIntoIteratory saber cuál corresponde a cada caso.
El contrato mínimo: type Item y next
Iterator es un trait con un solo método obligatorio. Todo lo demás —map, filter, collect, sum, los setenta y tantos adaptadores y consumidores— tiene implementación por defecto construida sobre él. El contrato es:
pub trait Iterator {
type Item; // que tipo produce
fn next(&mut self) -> Option<Self::Item>; // el siguiente, o None si se acabo
// ...decenas de metodos con cuerpo por defecto
}
type Item es un tipo asociado: fija de una vez qué produce este iterador. next es el motor: cada llamada avanza el estado interno y devuelve Some(valor), o None cuando no queda nada. Que devuelva Option es lo que codifica “puede que haya, puede que no” sin un centinela mágico ni una excepción.
Un iterador de verdad
Construyamos uno que genere la sucesión de Fibonacci. El estado —los dos últimos términos— vive en el struct; next lo hace avanzar en cada paso.
struct Fibonacci {
actual: u64,
siguiente: u64,
}
impl Iterator for Fibonacci {
type Item = u64;
fn next(&mut self) -> Option<u64> {
let emitido = self.actual;
self.actual = self.siguiente;
self.siguiente = emitido + self.siguiente;
Some(emitido) // nunca se agota: es infinito
}
}
fn fib() -> Fibonacci {
Fibonacci { actual: 0, siguiente: 1 }
}
Este iterador es infinito: su next jamás devuelve None. No pasa nada: la pereza permite iteradores sin fin siempre que un adaptador como take los recorte. Y en cuanto existe next, todo el vocabulario del trait está disponible sin escribir una línea más:
let primeros: Vec<u64> = fib().take(10).collect();
assert_eq!(primeros, [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]);
let pares: Vec<u64> = fib().filter(|n| n % 2 == 0).take(4).collect();
assert_eq!(pares, [0, 2, 8, 34]);
El contrato informal dice que, una vez que next devuelve None, las llamadas siguientes deberían seguir devolviendo None: el iterador está agotado. No es una regla que el compilador imponga, pero muchos adaptadores la suponen. Un iterador que “revive” tras agotarse —vuelve a dar Some— es fuente de errores sutiles al combinarlo con chain, zip o flatten. Si dudas, el adaptador fuse garantiza ese comportamiento envolviendo cualquier iterador.
Todo gratis por encima de next, e IntoIterator
La lección de diseño es contundente: implementas un método y recibes decenas. map, filter, take, zip, sum, fold, collect… todos llaman a next por debajo. Puedes redefinir alguno —size_hint para pre-reservar memoria, o nth para saltar sin recorrer— cuando conoces un atajo, pero nunca es obligatorio.
Conviene no confundir dos traits vecinos. Iterator es algo que produce elementos con next. IntoIterator es algo que puede convertirse en un iterador con into_iter; es lo que habilita a un tipo a usarse directamente en un bucle for. Un Vec no es un Iterator, pero sí es IntoIterator; por eso for x in vec funciona, mientras que for x in vec.iter() recorre el iterador que iter devuelve.
struct Cuenta {
fin: u32,
i: u32,
}
impl Iterator for Cuenta {
type Item = u32;
fn next(&mut self) -> Option<u32> {
if self.i < self.fin {
self.i += 1;
Some(self.i)
} else {
None // aqui si se agota
}
}
}
fn main() {
// Como Cuenta ya es Iterator, sum vino gratis con el contrato
let total: u32 = Cuenta { fin: 5, i: 0 }.sum();
assert_eq!(total, 15); // 1 + 2 + 3 + 4 + 5
}
flowchart LR call[Alguien llama a next] --> est[Lee y avanza el estado interno] est --> hay[Queda algo devuelve Some del valor] est --> no[Se agoto devuelve None] hay --> call style call fill:#cba6f7,color:#11111b style hay fill:#a6e3a1,color:#11111b style no fill:#f38ba8,color:#11111b
Hay una elegancia casi austera en cómo Rust define la iteración: reduce toda la idea de “recorrer una secuencia” a un único punto de contacto, next, y erige sobre él un edificio de setenta métodos sin pedirte que escribas ninguno. Esto es la interfaz mínima llevada al extremo, y encierra una lección que trasciende a los iteradores. Cuando concentras un concepto en el método más pequeño posible —el que ya no puede descomponerse más—, cada implementador paga el mínimo esfuerzo y hereda el máximo poder, y cada mejora en los métodos por defecto beneficia, a la vez, a todos los tipos que alguna vez firmaron el contrato. Tu Fibonacci de seis líneas gana sum, zip, flat_map y collect a Result sin saber que existen, porque no dependen de él: dependen de next, y next es lo único que prometiste. Compáralo con las jerarquías de herencia donde para “ser iterable” hay que extender una clase base gorda, sobrescribir varios métodos y encajar en un molde rígido. Aquí no hay molde: hay un contrato de una línea y una promesa de comportamiento —devuelve Some mientras haya, None cuando no, y quédate en None—. La consecuencia es que la iteración en Rust no es un privilegio de las colecciones de la biblioteca estándar, sino una capacidad que cualquiera puede conferir a cualquier tipo: un generador de números, un lector de líneas de un archivo, un recorrido en profundidad de un árbol, un flujo de eventos de red. Todos hablan el mismo idioma, todos componen entre sí, todos se benefician del mismo optimizador que funde la cadena en un bucle. La lección para el diseñador de bibliotecas es imborrable: busca el next de tu dominio —el átomo irreducible de tu abstracción— y construye lo demás como cortesía por defecto encima de él. La riqueza no viene de una interfaz enorme, sino de una interfaz diminuta multiplicada por todo lo que puedes derivar de ella.
- Implementa
Iteratorpara unstruct Contadorque emita de 1 any devuelvaNoneal final; consúmelo con un buclefor. - Escribe un iterador infinito de potencias de dos y usa
take_whilepara quedarte con las menores que mil. - Sobre tu
Fibonacci, comprueba quemap,filterysumfuncionan sin haberlos implementado; explica de dónde salen. - Añade una implementación de
size_hinta tu contador y razona cómo podría ayudar acollecta pre-reservar elVec. - Explica con un ejemplo la diferencia entre
IteratoreIntoIterator, y por quéfor x in vecusa el segundo.