Rusty Full Stack

El blog para los amantes de Rust, Ionic y Vuejs

Jaime Blandón
Jaime Blandón Desarrollador de software desde 2009, entusiasta de Rust, Vuejs y Ionic!. Fundador de este blog, espero que las publicaciones te sean de utilidad y si tienes comentarios para mejorar, son bienvenidos.

Iteradores En Rust Con Ejemplos

En esta publicación vamos a aprender sobre iteradores en Rust, para qué sirven y cómo utilizarlos.

 ¿Qué son los iteradores y en qué se diferencian de las listas o colecciones?

Tanto en Rust como en muchísimos lenguajes de programación, existen las estructuras de datos conocidos como arrays, listas o colecciones las cuales sirven para crear un conjunto de datos que se relacionan de alguna u otra manera. Dependiendo del lenguaje de programación, estas colecciones o listas podrán estar organizadas de forma consecutiva en la memoria de nuestro programa o pueden llegar a tener ubicaciones en memoria que no son consecutivas pero un índice o apuntador se encargan de ayudar a nuestro programa a ubicar como es qué nuestra lista o colección se encuentra armada.

Aunque en Rust, tenemos una forma directa de acceder a estas listas utilizando indices, por ejemplo:

        
        
            
let a = vec![1, 2, 3];
assert_eq!(a[0], 1); // Primer elemento de la lista
        
        
    

En donde los indices empiezan en "0", realizar otras operaciones como filtrar, sumar los elementos, unir dos listas, puede volverse más complicado y nos tocaría echar mano y construir muchos algoritmos ya conocidos de búsqueda y ordenamiento, es por ello que lo mejor es utilizar los iteradores.

Los iteradores no son más que una abstracción o representación de nuestra lista o colección que nos facilitará el poderla manipular y hacer más rápidamente las tareas de búsqueda de demás operaciones con nuestra colección.

¿Qué podemos hacer con los iteradores?

En Rust, existen muchas tareas que podemos realizar con los iteradores, de hecho, son tantas que se hace imposible cubrirlas en este artículo, así que aprenderemos a utilizar aquellas, sin menospreciar a las demás, que me han parecido bastante interesantes.

Entre las operaciones que existen se encuentran:

  • Recorrer el iterador.
  • Sumar los elementos.
  • Unir listas.
  • Realizar filtros.
  • Búsquedas.
  • Map o convertir nuestra lista en otra con alguna regla que necesitemos.
  • Muchas más.

Para ver todas las actividades que un iterador puede hacer, te recomiendo muchísimo echarle un vistazo a la documentación oficial en este enlace:

Documentación Iteradores en Rust.

Ejemplo de Iteradores En Rust.

Vamos a aprender a utilizar iteradores siguiendo algunos ejemplos prácticos, lo primero que vamos a hacer es aprender los conceptos básicos sobre como construir un iterador y como recorrerlo.

En esta ocasión no vamos a utilizar Cargo para crear nuestros ejemplos ya que no utilizaremos ningún crate externo y la idea es tener cada ejemplo en un archivo ejecutable diferente.

Comencemos creando un folder dónde más nos resulte fácil.

mkdir ejemplos_iteradores.

cd ejemplos_iteradores.

Puedes ver los códigos completos de los ejemplos en nuestro repositorio en github dando click acá.

Construir un Iterador En Rust.

Creemos ahora un archivo y le ponemos por nombre ejemplo_basico.rs, en este archivo vamos a aprender a crear un iterador y también la forma más simple para recorrerlo.

Dentro del nuevo archivo colocamos el siguiente código:

        
        
            // ejemplo_basico.rs

fn main() {
  let a = vec![1, 2, 3, 4];

  // Construimos el iterador
  let mi_iterador = a.iter();
}
        
        
    

El código anterior no hace mayor cosa, solamente crea un vector o lista de números del 1 al 4 y esta línea es la que se encarga de crear el iterador:

let mi_iterador = a.iter();

Por el momento no estamos utilizando nuestro iterador únicamente lo hemos construido, vamos a agregar algunas líneas para recorrer el iterador, vamos a utilizar el macro assert_eq! para evaluar si el valor que obtenemos es el que esperamos.

(Si quieres aprender más sobre assert_eq! puedes ver nuestra mini-serie de pruebas unitarias con Rust 😄)

Para recorrer un iterador, la forma principal de hacerlo es utilizando el método next(), el cual puede retornar algún valor (Some) si el iterador no ha llegado al final a None, que es cuando el iterador se pasa del último elemento, completemos el código anterior de la siguiente manera:

        
        
            // ejemplo_basico.rs

fn main() {

    // Este es un vector de numeros enteros
    let a = vec![1, 2, 3, 4];

    // el metodo iter() se encarga de crear un iterador.
    let mut mi_iterador = a.iter();

    // el iterador toma posesion del vector y utiliza next() para conseguir el siguiente
    // valor, se utiliza Some porque cuando el iterador llega a su fin
    // puede retornar un None
    assert_eq!( mi_iterador.next() , Some(&1) );
    assert_eq!( mi_iterador.next() , Some(&2) );
    assert_eq!( mi_iterador.next() , Some(&3) );
    assert_eq!( mi_iterador.next() , Some(&4) );
    assert_eq!( mi_iterador.next() , None );

}
        
        
    

Como podemos ver en las líneas con el assert_eq! nos vamos moviendo de elemento en elemento en el iterador hasta alcanzar el último elemento y luego nos retorna un None, si compilamos y ejecutamos el programa vamos a ver que no se produce ningún error de comparación.

Recuerda que para compilar un programa sin cargo, utilizamos rustc

 rustc ejemplo_basico.rs

y podemos ejecutar el ejecutable con:

./ejemplo_basico

Veremos que en nuestra terminal no muestra ningún mensaje de error lo que quiere decir que hemos logrado recorrer nuestro iterador de uno en uno.

Si bien es cierto el ejemplo anterior no es muy práctico porque muchas veces vamos a recorrer una lista utilizando algún loop como una instrucción for, ahora vamos a ver otro ejemplo para recorrer un iterador, pero está vez lo que haremos es que no solamente vamos a recorrer para verificar el valor, sino que ahora vamos a verificar también el índice, esto es muy útil cuando no solamente queremos saber el valor de un elemento sino también su índice.

Indices De Elemento de Una Lista.

creemos un nuevo archivo dentro de la misma carpeta que estamos utilizando, le pondremos de nombre ejemplo_indices.rs, la idea será que esta vez vamos a crear una lista de Strings, y vamos a mostrarla en pantalla junto con su índice.

Para poder obtener el índice, vamos a necesitar "enumerar" nuestro iterador, para ello ya existe un método que podemos utilizar llamado enumerate.

Coloquemos el siguiente código en nuestro nuevo archivo:

        
        
            // ejemplo_indices.rs

fn main() {

    let lista = vec!["Primer Elemento", "Segundo Element", "Tercer Elemento"];

    let mut mi_iter = lista.iter().enumerate();

}
        
        
    

El ejemplo anterior únicamente crea una lista de Strings y un iterador "enumerado" o con sus índices, ahora vamos a recorrerlo utilizando un while e imprimir en pantalla el valor de un elemento pero también su índice, hay que recordar que los índices en Rust inician con "0".

Completemos el código de esta forma:

        
        
            // ejemplo_indices.rs

fn main() {

    let lista = vec!["Primer Elemento", "Segundo Element", "Tercer Elemento"];

    let mut mi_iter = lista.iter().enumerate();

    while let Some((indice, valor)) = mi_iter.next() {
        println!("{} - {}", indice, valor); 
    }

}
        
        
    

El bucle:

while let Some((indice, valor)) = mi_iter.next();

Recorre nuestro iterador hasta encontrar el None y los valores, que en este caso son el indice y el valor los asigna en variables con el mismo nombre, luego los imprimimos en pantalla.

Para verificar que hace lo que esperamos, compilemos nuestro programa con:

rustc ejemplo_indices.rs

Ahora lo ejecutamos:

./ejemplo_indices

Si todo ha salido bien deberíamos de ver el siguiente mensaje en pantalla.

Del lado izquierdo, podemos ver el índice (que inicia en cero) y luego muestra el valor, así hasta llegar al último elemento, cuando encuentra un None, entonces el while ya no continúa.

Unir Elementos De Una Lista Por Pares.

Existe un método que permite unir iteradores para crear una nueva lista unificada por pares, este comportamiento lo podemos lograr utilizando el método zip, un ejemplo es que si tenemos estas listas:

lista 1 = [1, 2]

lista 2 = [3, 4]

El método zip, creará una nueva tercera lista donde unificará los elementos por pares, por ejemplo unirá el primer elemento de la lista 1 con el primer elemento de la lista 2, es decir que el zip resultante de ambas listas seria:

resultado = [[1, 3], [2, 4]]

Este tipo de operaciones es bastante útil para operaciones matemáticas en un plano cartesiano.

Hagamos un ejemplo creando un nuevo archivo llamado ejemplo_unir_listas.rs, y coloquemos el siguiente código:

        
        
            // ejemplo_unir_listas.rs

fn main() {

    let lista1 = vec!["primer elemento lista 1", "segundo elemento lista 1", "tercer elemento lista 1"];
    let lista2 = vec!["primer elemento lista 2", "segundo elemento lista 2", "tercer elemento lista 2"];

    let iterador_lista1 = lista1.iter();
    let iterador_lista2 = lista2.iter();

    let mut lista_unificada = iterador_lista1.zip(iterador_lista2);

    while let Some((elemento_lista1, elemento_lista2)) = lista_unificada.next() {
        println!("[{}, {}]", elemento_lista1, elemento_lista2);
    }

}
        
        
    

Como podemos ver en el ejemplo anterior, la lista_unificada une en pares los elementos del iterador de la lista 1 con los elementos del iterador de la lista 2:

        
        
            
let mut lista_unificada = iterador_lista1.zip(iterador_lista2);
        
        
    

Si ahora compilamos con:

rustc ejemplo_unir_listas.rs

Y ejecutamos con:

./ejemplo_unir_listas

Vemos que crea un tercer iterador con una lista en la cual ha ordenado por pares:

Sumar Elementos De Una Lista.

Existe un método para sumar los elementos de una lista, obviamente solo funcionará con listas númericas.

Crea un nuevo archivo y nómbralo como ejemplo_sumar_elementos.rs y coloca el siguiente código:

        
        
            fn main() {

    let elementos = vec![10, 25, 50, 100, 201, 23];

    let resultado: i32 = elementos.iter().sum();

    println!("Resultado Suma = {}", resultado);

}
        
        
    

Como puedes ver en el ejemplo anterior estamos creando una lista llamado "elementos" con valores numéricos de lo más variados, luego en la línea para obtener el resultado estamos utilizando:

elementos.iter().sum();

El cual retorna la suma de los elementos de la lista, si compilamos nuestro programa:

rustc ejemplo_sumar_elementos.rs

Veremos que en pantalla nos mostrará la suma como resultado, esto es bastante útil para listas muy largas, así evitamos construir un bucle solamente para sumarlos.

Transformar Una Lista Con Iteradores.

Existe un método llamado map() el cual sirve para poder transformar una lista a otra, es bastante útil cuando queremos hacer conversiones o cálculos en una lista y generar una nueva con los nuevos valores.

Hay algo importante que mencionar y es que los iteradores son lazy, es decir que hay que ejecutarlos explícitamente (como por ejemplo invocando el next(), sum(), etc), en el caso de map, no utiliza ninguna de los métodos anteriores, sino que será necesario utilizar un método llamado collect() que nos devolverá la nueva lista como respuesta, pero es de notar que devuelve la lista ya transformada NO un iterador.

Supongamos este ejemplo, imagina que tenemos una lista de números que representan los valores totales a pagar en una venta, pero el formato numérico no es lo que necesitamos, sino, crear una estructura nueva que contenga el valor total de la lista pero le agregamos un campo adicional para tener el mismo valor pero con un 10% de descuento. Hagamos este nuevo ejemplo creando un archivo y lo nombramos como ejemplo_transformacion_iteradores.rs.

Comencemos construyendo nuestra estructura y el array de totales a pagar.

        
        
            // ejemplo_transformacion_iteradores.rs

struct Venta {
    total_sin_descuento: f32,
    total_con_descuento: f32
}

fn main() {
    let totales = vec![25.35, 12.25, 10.15, 20.55];
}
        
        
    

Hasta acá solamente tenemos la estructura que queremos como resultado y el vector de totales, ahora haremos uso de map.

        
        
            // ejemplo_transformacion_iteradores.rs

struct Venta {
    total_sin_descuento: f32,
    total_con_descuento: f32
}

fn main() {
    let totales = vec![25.35, 12.25, 10.15, 20.55];
    let estructura_totales_con_descuentos: Vec<Venta> = totales.iter().map(
        |elemento| Venta{
            total_sin_descuento: *elemento,
            total_con_descuento: *elemento - (*elemento * 0.1) // 10 porciento menos, por facilidad
                                                               // no hacemos redondeos
        }
    ).collect(); // Es importante utilizar el collect porque los iteradores son lazy, es decir el
                 // map no se ejecutara si no se es requerido explicitamente con el collect

    // En esta ocasion por facilidad no hacemos redondeos, si quieres redondear algo
    // el crate con mejor precision para hacerlo es round
    // https://crates.io/crates/round
    for venta in estructura_totales_con_descuentos {
        println!("total: ${} - total con descuento: ${}", venta.total_sin_descuento, venta.total_con_descuento);
    }
}
        
        
    

Del código anterior lo importante es esta instrucción:

        
        
            

let estructura_totales_con_descuentos: Vec<Venta> = totales.iter().map(
        |elemento| Venta{
            total_sin_descuento: *elemento,
            total_con_descuento: *elemento - (*elemento * 0.1) // 10 porciento menos, por facilidad
                                                               // no hacemos redondeos
        }
    ).collect();
        
        
    

en ella construimos el iterador y configuramos un map, en el cual estamos creando un objeto de tipo Venta por a cada elemento dentro de nuestro iterador (lista numérica de totales) y colocamos el 10% de descuento. Luego es importante notar la invocación de collect() que como hemos dicho hace que el iterador ejecute el map y nos devuelva la lista deseada que es un Vec de Ventas

NOTA: por facilidad no estamos haciendo redondeos, si quisieras redondear los valores a 2 cifras decimales puedes hacerlo con el crate recomendado round, el cual maneja una precisión bastante buena.

Si compilamos nuestro código, vemos que map ha hecho una transformación a una nueva lista que ya no es numérica sino que de la estructura Venta.

rustc ejemplo_transformacion_iteradores.rs

./ejemplo_transformacion_iteradores

Buscar Un Elemento De Una Colección.

Los iteradores nos permiten también buscar un elemento en una colección, esto es bastante útil cuando necesitamos encontrar un elemento con alguna característica que sea única para el elemento en la colección, por ejemplo buscar a una persona por su identificador o a un estudiante por su número de estudiante, etc.

El método para buscar un elemento se llama find() y le pasamos como parámetro la característica de búsqueda (como el identificador de una persona), entonces el parámetro devolverá el primer elemento que encuentre que cumple con esa característica y si no encuentra nada, entonces devolverá un None.

Probemos esta funcionalidad con un ejemplo.

Vamos a hacer un programa y crearemos una lista de personas, cada persona tendrá un identificador que asumiremos es único y no se repite entre todas las personas de la lista, entonces, vamos a realizar una búsqueda entre todas las personas y mostraremos en pantalla el resultado del elemento encontrado.

Empecemos creando un archivo que llamaremos ejemplo_find_iteradores.rs y vamos a colocar el siguiente código:

        
        
            // ejemplo_find_iteradores.rs

struct Persona {
    edad: u8,
    nombre: String,
    identificador: String
}

fn main() {
    let personas: Vec<Persona> = vec![
        Persona {
            edad: 25,
            nombre: "Juan".to_string(),
            identificador: "001".to_string()
        },
        Persona {
            edad: 18,
            nombre: "Maria".to_string(),
            identificador: "002".to_string()
        },
        Persona {
            edad: 35,
            nombre: "Ana".to_string(),
            identificador: "003".to_string()
        },
        Persona {
            edad: 30,
            nombre: "Francisco".to_string(),
            identificador: "004".to_string()
        }
    ];

    let mut iterador_personas = personas.iter();
}
        
        
    

El código anterior únicamente crea una estructura llamada Persona, en la cual le asignamos los siguientes atributos:

  • Edad
  • Nombre
  • Identificador (debe ser único)

Luego creamos una lista con 4 personas y a cada una le ponemos un nombre, su edad, y un identificador, por último, creamos un iterador que hemos llamado iterador_personas.

Ahora vamos a realizar la búsqueda, como hemos comentado, el método find, puede devolver un elemento o None si no encuentra ningún elemento, para ello completemos el código, y quedará de la siguiente forma:

        
        
            // ejemplo_find_iteradores.rs

struct Persona {
    edad: u8,
    nombre: String,
    identificador: String
}

fn main() {
    let personas: Vec<Persona> = vec![
        Persona {
            edad: 25,
            nombre: "Juan".to_string(),
            identificador: "001".to_string()
        },
        Persona {
            edad: 18,
            nombre: "Maria".to_string(),
            identificador: "002".to_string()
        },
        Persona {
            edad: 35,
            nombre: "Ana".to_string(),
            identificador: "003".to_string()
        },
        Persona {
            edad: 30,
            nombre: "Francisco".to_string(),
            identificador: "004".to_string()
        }
    ];

    let mut iterador_personas = personas.iter();

    let identificador_a_buscar = "001".to_string();

    if let Some(persona) = iterador_personas.find(|&p| p.identificador == identificador_a_buscar) {
        println!("Persona Encontrada:");
        println!("Identificador: {}", persona.identificador);
        println!("Nombre: {}", persona.nombre);
        println!("Edad: {}", persona.edad);
    } else {
        println!("Persona no encontrada");
    }
}
        
        
    

En el código anterior, hemos creado una variable llamada identificador_a_buscar, esa variable contendrá el identificador que queremos encontrar en la lista de personas, luego hemos colocado la siguiente condición:

        
        
            

if let Some(persona) = iterador_personas.find(|&p| p.identificador == identificador_a_buscar) {
        println!("Persona Encontrada:");
        println!("Identificador: {}", persona.identificador);
        println!("Nombre: {}", persona.nombre);
        println!("Edad: {}", persona.edad);
} else {
        println!("Persona no encontrada");
}
        
        
    

La condición anterior indica que si encuentra una persona entonces imprimirá en pantalla los datos de la persona encontrada, pero si no encuentra a nadie, entonces imprimirá "Persona no encontrada"

Si miramos esta instrucción:

        
        
            

iterador_personas.find(|&p| p.identificador == identificador_a_buscar
        
        
    

Podemos ver que el closure &p va tomando elemento por elemento y va comparando si su identificador es igual al identificador que estamos buscando. En el ejemplo hemos colocado que busque el identificador "001", compilemos el código y miremos el resultado.

rustc ejemplo_find_iteradores.rs

y ahora lo ejecutamos

./ejemplo_find_iteradores

Vamos a ver que en pantalla encuentra a "Juan" que es precisamente la persona con el identificador "001":

Probemos ahora cambiando el valor de la variable identificador_a_buscar y coloquemos el valor "002", volvamos a compilar y ejecutar nuestro programa.

rustc ejemplo_find_iteradores.rs

./ejemplo_find_iteradores

Vemos que esta vez encuentra a "Maria" quien tiene el identificador "002".

Genial! nuestro programa parece buscar como esperamos, pero ahora coloquemos a la variable identificador_a_buscar un identificador que no exista como "005", compilemos nuevamente nuestro programa y lo ejecutamos:

rustc ejemplo_find_iteradores.rs

./ejemplo_find_iteradores

Como no hay ninguna persona con el identificador, nuestro programa muestra que no ha encontrado ninguna persona.

Filtrar Elementos De Una Colección.

Al igual que el find() que nos regresa un elemento (o None), podemos también filtrar una sub colección de elementos que cumplan con alguna característica, en este caso, vamos a obtener otro iterador, no solamente un elemento.

Re-utilicemos nuestro código anterior donde teníamos una lista de personas, esta vez, vamos a hacer que nuestro programa filtre a todas las personas que tengan más de cierta edad. Por motivos de aprendizaje, vamos a hacer este programa en un nuevo archivo, vamos a nombrarlo ejemplo_filter_iteradores.rs y colocamos el mismo código de las personas hasta la línea de creación del iterador.

        
        
            // ejemplo_filter_iteradores.rs

struct Persona {
    edad: u8,
    nombre: String,
    identificador: String
}

fn main() {
    let personas: Vec<Persona> = vec![
        Persona {
            edad: 25,
            nombre: "Juan".to_string(),
            identificador: "001".to_string()
        },
        Persona {
            edad: 18,
            nombre: "Maria".to_string(),
            identificador: "002".to_string()
        },
        Persona {
            edad: 35,
            nombre: "Ana".to_string(),
            identificador: "003".to_string()
        },
        Persona {
            edad: 30,
            nombre: "Francisco".to_string(),
            identificador: "004".to_string()
        }
    ];

    let iterador_personas = personas.iter();
}
        
        
    

Ahora vamos a agregar realizar nuestro filtro y vamos a imprimir todas las personas que tengan 30 años o más.

        
        
            // ejemplo_filter_iteradores.rs

struct Persona {
    edad: u8,
    nombre: String,
    identificador: String
}

fn main() {
    let personas: Vec<Persona> = vec![
        Persona {
            edad: 25,
            nombre: "Juan".to_string(),
            identificador: "001".to_string()
        },
        Persona {
            edad: 18,
            nombre: "Maria".to_string(),
            identificador: "002".to_string()
        },
        Persona {
            edad: 35,
            nombre: "Ana".to_string(),
            identificador: "003".to_string()
        },
        Persona {
            edad: 30,
            nombre: "Francisco".to_string(),
            identificador: "004".to_string()
        }
    ];

    let iterador_personas = personas.iter();
    
    let edad_minima_a_buscar = 30;

    // El resultado de un filter es otro iterador.
    let personas_encontradas = iterador_personas.filter(|&p| p.edad >= edad_minima_a_buscar);
    let mut cantidad_personas_encontradas = 0;

    for persona in personas_encontradas {
        println!("Persona Encontrada:");
        println!("Identificador: {}", persona.identificador);
        println!("Nombre: {}", persona.nombre);
        println!("Edad: {}", persona.edad);
        println!("=========================================");
        cantidad_personas_encontradas += 1;
    }

    println!("{} Personas Encontradas", cantidad_personas_encontradas);
}
        
        
    

En el código anterior realizamos el filtro con esta línea de código:

        
        
            

iterador_personas.filter(|&p| p.edad >= edad_minima_a_buscar);

        
        
    

Luego imprimimos en pantalla los resultados obtenidos.

Compilemos nuestro programa y lo ejecutamos.

rustc ejemplo_filter_iteradores.rs

./ejemplo_filter_iteradores

Veremos que en efecto nuestro programa ha filtrado las personas con 30 años o más

Conclusiones.

En este artículo hemos explorado solamente una pequeña parte de lo que puedes hacer con iteradores, pero espero haya sido ilustrativo y te anime a continuar aprendiendo más sobre ellos.

Hemos también aprendido que manejar iteradores nos facilitará de sobremanera nuestras actividades ya sean laborales o académicas.

Puedes ver los códigos completos de los ejemplos en nuestro repositorio en github dando click acá.

Si este artículo te ha sido de utilidad compártelo en tus redes y con tus amistades 😄

Escribe en la sección de comentarios aquellos temas sobre Rust que te gustaría aprendamos juntos y no olvides seguirme en twitter.

println!("Hasta la próxima");


 Utilizamos cookies propias y de terceros para mejorar tu experiencia, mostrar publicidad y análisis de navegación, puedes encontrar el detalle en nuestra Política de Cookies