Bienvenidos sean a este post, hoy hablaremos sobre el concepto de iteradores.
Cuando hablamos de iteradores nos referimos a un patron que nos permite ejecutar alguna tarea sobre una secuencia de elementos por turno, siendo este el responsable por la logica de la iteracion sobre cada elemento y determinando cuando se finalizo la secuencia pero en Rust los iteradores son «peresozos» porque no se utilizan hasta que no se llama a un metodo que lo aplique, vamos a verlo en accion con un simple ejemplo:
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
for val in v1_iter
{
println!("{}", val);
}
Primero creamos una variable que sera un vector donde contendra tres valores, la siguiente variable creara un iterador sobre esta variable pero no hara nada, en cambio en el bucle usaremos a cada uno de los elementos del iterador por cada pasada y lo mostraremos en pantalla, con esta logica para lenguajes que no posean iteradores en sus librerias estandar escribiriamos una funcion que comenzaria una variable desde el indice 0, a su vez usando esa variable para indexar en el vector, obtener el valor de la posicion y seguir incrementando el valor de la variable en un ciclo hasta alcanzar el total de los elementos o valores del vector.
Por suerte los iteradores hacen todo eso por nosotros y nos evitan tener que reinventar la polvora y gracias a esto tenemos mucha flexibilidad porque nos permite tratar con otros tipos de secuencias y no solamente con estructura de datos que podamos indexar (vectores).
Todos los iteradores implementan un trait llamado Iterator, el cual es definido en la libreria estandar, su forma mas basica de trabajo es que el iterador necesita que definas un tipo de dato, a su vez este tipo de dato sera usado para devolverlo con el metodo next, a su vez el metodo next siempre devolvera un valor almacenado en Some pero cuando se termine devolvera un None indicando que se llego al final del iterador, si bien es un metodo interno y automatico nosotros podemos utilizarlo tambien, tomemos el ejemplo anterior y modifiquemoslo de la siguiente manera:
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
assert_eq!(v1_iter.next(), None);
Creamos el iterador como hicimos anteriormente pero esta vez utilizamos varios assert_eq para chequear que el valor devuelto por next coincide con el valor en la posicion de Some y en el ultimo caso con None, si lo razonamos estas lineas deberian devolver todo true porque coiniciden hasta con la cantidad de elementos y posiciones, esto es tambien debido a que el iterador siempre creara valores inmutables pero si nosotros tenemos la necesidad de trabajar con valores mutables en lugar de utilizar a iter deberiamos usar a iter_mut que es lo mismo pero con valores mutables, una curiosidad que debemos mencionar es que el bucle for al tomar control sobre el iterador transforma a este en mutable sin que nos enteremos nosotros.
Los iteradores poseen metodos que los consumen, ya nombramos uno como es next pero tambien tenemos uno llamado sum, el cual llamara a next para pasar por todos los elementos y sumarlos devolviendo el valor total de todos, por ejemplo si lo aplicaramos sobre el ejemplo anterior nos devolveria un valor de 6, veamos algunos disponibles:
- count, cuenta la cantidad de iteraciones (elementos) que posee
- last, nos devuelve el ultimo elemento del iterador
- advance_by, avanza la cantidad de elementos que le informemos
- chain, junta dos iteradores y lo transforma en una nueva secuencia
- for_each, llama a un closure por cada elemento de un iterador
- filter, crea un iterador con un closure para determinar si el elemento sera usado
- enumerate, nos devuelve el numero de iteracion actual y el siguiente valor
- map, toma un closure y crea un iterador que llama a ese closure por cada elemento
Como dijimos estos son metodos consumidores porque como dijimos al comienzo para poder utilizar al iterador debemos consumirlo o procesarlo, uno de los metodos que mencionamos entre los anteriores que nos permite crear otro iterador es map, vamos a tomar el ejemplo anterior y lo modificaremos de la siguiente manera:
let v1: Vec<i32> = vec![1, 2, 3];
v1.iter().map(|x| x + 1);
Observen como aplicamos un closure para map pero si lo compilaramos no solo nos devolveria un error de compilacion sino tambien lo siguiente:
= note: iterators are lazy and do nothing unless consumed
Como dijimos los iteradores son perezosos y no hacen nada sino los consumimos, por eso para que el codigo anterior funcionase debemos hacer lo siguiente:
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
for val in v2
{
println!("{}", val);
}
En este caso asignamos la iteracion a una nueva variable de tipo vector pero sin ningun tipo especifico pero con una curiosidad, al final agregamos collect para que transforme la iteracion en una coleccion, despues tenemos un bucle que pasara nuestra nueva variable y mostrara el resultado del closure de map, les dejo un ejemplo de salida:
tinchicus@dbn001vrt:~/lenguajes/rust/ejemplo02$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/ejemplo02`
2
3
4
tinchicus@dbn001vrt:~/lenguajes/rust/ejemplo02$
Para entender el concepto vamos a crear un ejemplo donde aplicaremos a Iterator para poder crear nuestro propio iterador, para ello generemos una nueva libreria o crate que llamaremos iterador, para ello deben usar el siguiente comando:
$ cargo new iterador --lib
Esto generara nuestra libreria, una vez creado modificaremos el codigo existente del archivo lib.rs con el siguiente:
lib.rs
struct Contador
{
contar: u32,
}
impl Contador
{
fn new() -> Contador
{
Contador { contar: 0 }
}
}
impl Iterator for Contador
{
type Item = u32;
fn next(&mut self) -> Option<Self::Item>
{
self.contar += 1;
if self.contar < 6
{
Some(self.contar)
} else {
None
}
}
}
#[test]
fn llamando_al_next()
{
let mut contador = Contador::new();
assert_eq!(contador.next(), Some(1));
assert_eq!(contador.next(), Some(2));
assert_eq!(contador.next(), Some(3));
assert_eq!(contador.next(), Some(4));
assert_eq!(contador.next(), Some(5));
assert_eq!(contador.next(), None);
}
Primero crearemos un struct que se llamara Contador, dentro tendra una propiedad llamada contar, lo siguiente sera una implementacion para el struct, en esta redefiniremos a new para que inicie el valor de contar de Contador, despues tendremos la siguiente implementacion:
impl Iterator for Contador
{
type Item = u32;
fn next(&mut self) -> Option<Self::Item>
{
self.contar += 1;
if self.contar < 6
{
Some(self.contar)
} else {
None
}
}
}
En esta ocasion implementaremos a Iterator para nuestro struct, lo primero que haremos sera definir un tipo Item, este es en realidad un tipo asociado y se lo denomina asi porque se asocian a los tipos de la implementacion o trait, donde usualmente se utilizan, despues definiremos una funcion que llamaremos next donde la salida sera de tipo Option, en este caso nos permitira el valor Some y None, tambien le especificamos que sea el valor del objeto actual mediante el Self::Item, dentro de esta funcion incrementaremos a contar para chequear mediante un condicional que mientras sea menor a 6 lo devolvermos con Some de lo contrario devolveremos un None, despues tenemos una funcion para llamarlo pero este posee un atributo de test para que lo verifiquemos mediante la opcion de test de cargo, aqui crearemos un nuevo objeto de Contador y luego por medio de assert_eq iremos chequeando que los next que ejecutemos sean igual a lo almacenado en las distintas posiciones de Some y el ultimo sea None, si lo testean deberia funcionar correctamente y pasar el test, mas adelante cuando hablemos sobre los tests profundizaremo sobre todo esto.
En resumen, hoy hemos visto a iteradores, lo perezosos que pueden ser, como se deben consumirlos para poder utilizarlos, algunos metodos para consumirlos y obtener resultados, tambien hemos visto como crear un iterador propio, espero les haya sido de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
