diff --git a/book/book.toml b/book/book.toml index 78ab4a0e5be49a0c31d47f9e83483f922c1b1294..767af31f2e9bf7eec1efa6f47bd121ed1e1efb36 100644 --- a/book/book.toml +++ b/book/book.toml @@ -5,6 +5,9 @@ multilingual = false src = "src" title = "Rust-101: Université d'été" +[output.html] +mathjax-support = true + [output.html.playground] editable = true diff --git a/book/src/part10.md b/book/src/part10.md index 1acace90d165643b41b496ef1c9c50e80ba5f0af..e4839da58b3d91a5d308b4d082b9510b978b35fd 100644 --- a/book/src/part10.md +++ b/book/src/part10.md @@ -1 +1,238 @@ -# Pointeurs intelligents et mémoire \ No newline at end of file +# Discussion du code `part10` + +## Concepts + +Les concepts abordés dans cet exemple sont: + +- [Discussion du code `part10`](#discussion-du-code-part10) + - [Concepts](#concepts) + - [Documentation](#documentation) + - [Discussion](#discussion) + - [Le trait itérateur](#le-trait-itérateur) + - [Les fonctions `iter` et `collect`](#les-fonctions-iter-et-collect) + - [Quelques fonctions d'ordre supérieur sur les itérateurs](#quelques-fonctions-dordre-supérieur-sur-les-itérateurs) + - [Performances et lazy evaluation](#performances-et-lazy-evaluation) + - [Rustlings](#rustlings) + - [Les itérateurs](#les-itérateurs) + +## Documentation + +Afin de compléter ce cours, je vous recommande la lecture des ressources suivantes : + +- [Traitements des collections avec de itérateurs en Rust](https://doc.rust-lang.org/book/ch13-02-iterators.html) +- [Le trait Iterator en Rust](https://doc.rust-lang.org/std/iter/trait.Iterator.html) +- [Les méthodes du trait Iterator](https://doc.rust-lang.org/std/iter/trait.Iterator.html#provided-methods) + +## Discussion + +### Le trait itérateur + +L'itérateur est un patron de conception extrêment répandu en prorgrammation orientée objet. On le retrouve +dans plusieurs langages comme par exemple python. + +Le trait `Iterator` est défini ainsi en version simplifiée dans la documentation : + +```rust +trait Iterator { + type Item; + fn next(&mut self) -> Option<Self::Item>; +} +``` + +Un itérateur possède donc un élément courant et une méthode `next()` qui permet de passer à l'élément suivant, +puis de retourner l'élément courant. Un itérateur peut-être fini ou infini. Il peut permettre par exemple de +parcourir une collection ou de créer un compteur. + +La documentation Rust offre [un exemple d'implémentation d'itérateur assez accessible](https://doc.rust-lang.org/std/iter/index.html#implementing-iterator). + +### Les fonctions `iter` et `collect` + +Le Rust propose des méthodes permettant de passer d'une collection à un itérateur assez facilement. + +```rust +# fn main(){ + let v : Vec<i32> = vec![1,2,3,4]; + let mut v_iter = v.iter(); + + println!("{}", v_iter.next().unwrap()); + println!("{}", v_iter.next().unwrap()); + println!("{}", v_iter.next().unwrap()); + println!("{}", v_iter.next().unwrap()); + assert!(v_iter.next().is_none()); +# } +``` + +La méthode `iter()` des vecteurs permet de créer un itérateur à partir d'un vecteur. Il faut noter que `v_iter` est mutable, car son état interne est **modifié** par l'appel à la méthode `next()`. +En effet, pour pouvoir avancer dans notre itérateur, nous utilisons la fonction `next()` qui avance l'élément courant d'une position +et retourne une `Option` sur l'élément courant. Pour rappel, la signature de la méthode de `next()` est `fn next(&mut self) -> Option<Self::Item>;`. + +Il est aussi possible de transformer un itérateur en collection à l'aide de la méthode `collect()`. La fonction `from_fn()` permet +de créer un itérateur à l'aide d'une fonction : + +```rust +# fn main(){ + let mut count = 0; + let even_counter = std::iter::from_fn(|| { + count += 2; + Some(count) + }); + + let v : Vec<i32> = even_counter.take(3).collect(); + println!("Les 3 premiers nombres paires sont {}, {}, {}", v[0], v[1], v[2]) +# } +``` + +Le code ci-dessus commence par créer un itérateur infini de nombres paires. Nous verrons plus-tard que les éléments de l'itérateur infini n'est pas généré immédiatement +[dans la section lazy-evaluation](#performances-et-lazy-evaluation). +Nous utilisons une variable mutable `count`, afin de générer les valeurs de notre itérateur. +Notre closure va capturer cette variable et l'incrémenter de 2 à chaque appel et retourner la valeur courante encapsulée dans une `Option`. + +Notre itérateur étant infini, nous devons le limiter à un nombre fini d'éléments avant de pouvoir récupérer une collection. +Pour cela nous utilisons la méthode `take()` qui permet de limiter la taille de notre itérateur. Finalement, nous pouvons +récupérer un vecteur d'entier à l'aide de la méthode `collect()`. + +### Quelques fonctions d'ordre supérieur sur les itérateurs + +L'intérêt principal des itérateurs en Rust réside dans ses fonctions d'ordre supérieur. Elle permettent de traiter des collections +de manière efficaces en apportant des garanties notament en terme de concurrence. On retrouve notament le crate +[Rayon-rs](https://github.com/rayon-rs/rayon) qui permet d'effectuer des traitement en parallèle sans apporter de modifications +majeures au code. + +Ici, nous nous concentrerons sur les méthodes suivantes qui sont très communes dans les langages fonctionnels (pas toujours avec les mêmes noms) : + +- `map()` +- `filter()` +- `fold()` +- `zip()` + +Reprenons notre code de recherche du minimum d'une liste : + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_minimum}} +``` + +Notre fonction prend en argument un vecteur `v` que nous transformons en itérateur avec la méthode `iter()`. Pour trouver le minimum +de notre itérateur, nous utilisons la méthode `fold()`. Cette méthode permet de réduire un itérateur en appliquant itérativement, +une fonction qui prend deux arguments et qui retourne un seul élément. On se retrouve donc avec une +seule valeur à la fin de la réduction. + +La méthode `fold()` prend donc deux arguments. Le premier est la valeur d'initialisation de notre réduction. On parle parfois d'élément neutre, +mais ce terme est plus restrictif qu'une simple valeur initiale. + +L'élément neutre d'une opération est l'élément `N` qui si on lui applique l'opération avec n'importe quel autre élément `x` retourne +cet élément `x`. Si nous prenons l'exemple de l'addition, l'élément neutre est 0, car : +$$ 0 + x = x + 0 = x $$ +et ce peu importe la valeur de x. Il est préférable d'utiliser un élément neutre, plutôt qu'une valeur quelconque pour l'initialisation +de notre réduction. Sans rentrer dans les détails, qui dépassent le cadre de ce cours, les algoritmes parallèles de réduction reposent +sur l'usage d'un élément neutre. Utiliser un élément neutre, vous permettra donc de parallèliser votre code bien plus simplement. + +En l'occurence puisqu'on veut retourner une option, notre élément neutre est `None`. + +Le deuxième argument de notre fonction `fold()` est l'opération de réduction que nous utiliserons pour trouver le minimum. Pour cela nous +utilisons une closure. Cette dernière prend deux argument, un accumulateur (la valeur courante de la réduction) de type `Option<i32>` +et un l'élément courant de l'itérateur qui est de type `&i32`. Notre closure va simplement retourner une Option contenant l'élément le +plus petit entre la valeur actuelle de l'accumulateur et la valeur courante. Cette valeur devient la nouvelle valeur de l'accumulateur +pour l'appel suivant. + +Le résultat de la méthode `fold()` est la dernière valeur de l'accumulateur. Si l'itérateur est vide, la méthode `fold()` retournera la valeur +d'initialisation. Dans notre cas, il s'agit d'une `Option` vide. + +Pour illustrer l'usage de la méthode `filter()`, nous avons une fonction qui trouve le plus petit nombre pair dans un vecteur : + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_even_minimum}} +``` + +La méthode est similaire à la recherche du minimum, mais afin de garder uniquement les nombres pairs on ajoute la fonction `filter()`. Quand on ajoute ainsi +une série de traitements à la suite, on parle de pipelines. + +La fonction `filter()` prend une fonction en paramètre appelée prédicat. Un prédicat et une fonction qui prend un élément et retourne un booléen. +Cette fonction va déterminer quels éléments conserver. Ici nous utilisons une closure qui retourne `true` si le nombre est pair. La méthode `filter()` +va retourner un nouvel itérateur composé uniquement des éléments séléctionnés par le prédicat. + +Pour finir, on recherche la valeur minimum de ce nouvel itérateur, de la même façon que dans l'exemple +précédent, à l'aide de la méthode `fold()`. + +Essayons maintenant de résoudre un problème un peu plus complexe en ajoutant les méthodes `zip()` et `map()`. +Nous aimerions trouver quel est l'élément le plus petit en valeur absolue d'un vecteur donné. + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_absolute_minimum}} +``` + +La première étape consiste à créer deux itérateurs. Le premier contient le signe de chaque élément +et le deuxième, la valeur absolue de chaque élément. + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_absolute_minimum_1}} +``` + +Pour obtenir ces itérateurs, nous allons transformer nos itérateurs obtenus avec `iter()` en utilsant +la fonction `map()`. Cette méthode permet d'appliquer une même transformation sur tous les éléments +d'un itérateur. Pour l'itérateur `signs`, on appelle la méthode `signum()` des `i32`, qui retourne 1 si +le nombre est positif, 0 si le nombre est 0 et -1 si le nombre est négatif. Pour `abs_values`, +nous utilisons la méthode `abs()` des `i32`, qui retourne la valeur absolue d'un nombre. + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_absolute_minimum_2}} +``` + +Maintenant que nous avons nos deux itérateurs, nous aimerions pouvoir itérer sur les deux simultanément. +Pour cela, nous pouvons utiliser la méthode `zip()`. Elle permet de transformer deux itérateurs, en un +unique itérateur de tuple. Ici nous avons deux itérateurs de `i32`, qui deviennent donc un seul itérateur +de type tuple `(i32, i32)`. + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_absolute_minimum_3}} +``` + +Ensuite avec `fold()`, il nous suffit de comparer les valeurs absolues et de retourner une option contenant +le signe et la valeur absolue. + +```rust,ignore +{{#include ../../codes/rust_lang/part10/src/find.rs:find_absolute_minimum_4}} +``` + +Pour finir, on utilise la méthode `map()` de notre `Option<(i32,i32)>` pour multiplier +la valeur absolue par le signe. Ce qui nous donne au final une `Option<i32>` contenant notre résultat. + +### Performances et lazy evaluation + +Les transformations sur les itérateurs sont en général aussi perfomantes qu'une +simple boucle for. On peut voir par exemple [ce benchmark dans le livre Rust](https://doc.rust-lang.org/book/ch13-04-performance.html). +Si on ajoute la possibilité de parallèliser facilement un code basé sur les itérateurs, +leur intérêt paraît évident. + +Un des éléments qui explique les perfomances des itérateurs réside dans la lazy evaluation. +Lorsqu'on appelle une opération de transformation sur un itérateur, la transformation n'est +pas réalisée directement. C'est uniquement lorsqu'on appelle une fonction dite terminale, comme +par exemple `fold()` ou `collect()` qui doivent produire un résulat. Les transformations intermédiaires +peuvent ainsi souvent être fusionnés. + +Ce qui veut dire par exemple que dans le code suivant, + +```rust +# fn main() { + let v = vec![0,1,2,3,4,5]; + let it = v.iter().map(|x| x + 1).map(|x| x * 3).filter(|x| x % 2 == 1); + let res : Vec<i32> = it.collect(); +# } +``` + +que tant que la fonction `collect()` n'a pas été appelée, alors aucune transformation n'est effectuée. +Si vous ne nous croyez pas, un moyen simple de vous en convaincre, consiste à appliquer +une série de transformation sur un itérateur infini et de mesurer la performance. + +## Rustlings + +Les rustlings à faire dans ce chapitre sont les suivants: + +### Les itérateurs + +```bash +$ rustlings run iterators1 +$ rustlings run iterators2 +$ rustlings run iterators3 +$ rustlings run iterators4 +$ rustlings run iterators5 +``` diff --git a/codes/rust_lang/part08/src/find.rs b/codes/rust_lang/part08/src/find.rs index 0d61d8eca1d8b755ec5b99b6ffc7e631b4e963b3..c8dccdd94b6a885957468b2b62ed635d945259d1 100644 --- a/codes/rust_lang/part08/src/find.rs +++ b/codes/rust_lang/part08/src/find.rs @@ -6,8 +6,8 @@ use crate::binary_operator::BinaryOperator; /// Computes the result of a binary reduction of an Array of a type T. /// Take the binary operation as a function. -/// Returns a [Option::Some] containing the the result value -/// or [Option::None] if the array was empty value was found. +/// Returns a [Option::Some] containing the result value +/// or [Option::None] if the array was empty. /// /// # Example /// diff --git a/codes/rust_lang/part10/Cargo.toml b/codes/rust_lang/part10/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..40bd900f032af074f2f57cab48d0709601a271d6 --- /dev/null +++ b/codes/rust_lang/part10/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "part10" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/codes/rust_lang/part10/src/find.rs b/codes/rust_lang/part10/src/find.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b0e6e3a5a6ebcd5c5df41063eb1d6a9a773d732 --- /dev/null +++ b/codes/rust_lang/part10/src/find.rs @@ -0,0 +1,109 @@ +//! Contains the core logic of the library, allowing to tore generic values +//! (or their absence) and manipulate them. +//! We demonstrates three kind of way to deal with errors + +/// Computes the minimum of a vector of i32. +/// Returns a [Option::Some] containing the minimum value +/// or [Option::None] if the vec was empty. +/// +/// # Example +/// +/// ``` +/// # use part10::find::{find_minimum}; +/// # fn main() { +/// let v = vec![-2, 5, 18, 65, 22, 56, -30]; +/// let min = find_minimum(&v); +/// assert!(min == Some(-30)); +/// # } +/// ``` +// ANCHOR: find_minimum +pub fn find_minimum(v: &Vec<i32>) -> Option<i32> { + v.iter().fold(None, |acc, current| { + let next_acc = if let Some(val) = acc { + if val <= *current { + val + } else { + *current + } + } else { + *current + }; + Some(next_acc) + }) +} +// ANCHOR_END: find_minimum + +/// Computes the smallest even number in a vector of i32. +/// Returns a [Option::Some] containing the smallest even number +/// or [Option::None] if the vec was empty. +/// +/// # Example +/// +/// ``` +/// # use part10::find::{find_even_minimum}; +/// # fn main() { +/// let v = vec![15, 64, 47, 2, 1, 53, 22]; +/// let min = find_even_minimum(&v); +/// assert!(min == Some(2)); +/// # } +/// ``` +// ANCHOR: find_even_minimum +pub fn find_even_minimum(v: &Vec<i32>) -> Option<i32> { + v.iter().filter(|i| *i % 2 == 0).fold(None, |acc, current| { + let next_acc = if let Some(val) = acc { + if val <= *current { + val + } else { + *current + } + } else { + *current + }; + Some(next_acc) + }) +} +// ANCHOR_END: find_even_minimum + +/// Computes the minimum absolute value of a vector of i32. +/// Returns a [Option::Some] containing the minimum abs value +/// or [Option::None] if the vec was empty. +/// +/// # Example +/// +/// ``` +/// # use part10::find::{find_absolute_minimum}; +/// # fn main() { +/// let v = vec![-2, 5, 18, 65, 22, 56, -30]; +/// let min = find_absolute_minimum(&v); +/// assert!(min == Some(-2)); +/// # } +/// ``` +// ANCHOR: find_absolute_minimum +pub fn find_absolute_minimum(v: &Vec<i32>) -> Option<i32> { + // ANCHOR: find_absolute_minimum_1 + let signs = v.iter().map(|i| i.signum()); + let abs_values = v.iter().map(|i| i.abs()); + // ANCHOR_END: find_absolute_minimum_1 + // ANCHOR: find_absolute_minimum_2 + signs + .zip(abs_values) + // ANCHOR_END: find_absolute_minimum_2 + // ANCHOR: find_absolute_minimum_3 + .fold(None, |acc, (c_sign, c_abs_v)| { + let next_acc = if let Some((sign, abs_v)) = acc { + if abs_v <= c_abs_v { + (sign, abs_v) + } else { + (c_sign, c_abs_v) + } + } else { + (c_sign, c_abs_v) + }; + Some(next_acc) + }) + // ANCHOR_END: find_absolute_minimum_3 + // ANCHOR: find_absolute_minimum_4 + .map(|(sign, abs_v)| sign * abs_v) + // ANCHOR_END: find_absolute_minimum_4 +} +// ANCHOR_END: find_absolute_minimum diff --git a/codes/rust_lang/part10/src/io.rs b/codes/rust_lang/part10/src/io.rs new file mode 100644 index 0000000000000000000000000000000000000000..a0e2acf0fc5f99d49229b13275f542c12e4e1692 --- /dev/null +++ b/codes/rust_lang/part10/src/io.rs @@ -0,0 +1,23 @@ +//! Contains functions to interact with the user, either +//! by reading inputs from the terminal, either by writing values +//! in it. + +/// Poorly emulates the parsing of a command line. +pub fn read_command_line_correct() -> Vec<i32> { + vec![-10, 32, 12, -43, 52, -53, 83, -2, 9] +} + +/// Poorly emulates the parsing of a command line. +pub fn read_empty_command_line() -> Vec<i32> { + vec![] +} + +/// Prints all the elements of the vector. +/// vector is borrowed here +pub fn print_vec(v: &Vec<i32>) { + print!("[ "); + for t in v { + print!("{} ", t); + } + println!("]"); +} diff --git a/codes/rust_lang/part10/src/lib.rs b/codes/rust_lang/part10/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..865bce85931b6656d9996d6624a971c796a41a60 --- /dev/null +++ b/codes/rust_lang/part10/src/lib.rs @@ -0,0 +1,28 @@ +//! This crate shows us different ways of dealing with errors in a Rust program. +//! You will find examples of [Option], [Result] and [panic!]. + +pub mod find; +pub mod io; + +#[cfg(test)] +mod tests { + use crate::find::{find_absolute_minimum, find_minimum}; + const VEC: [i32; 9] = [10, 32, 12, 43, 52, 53, 83, 2, 9]; + const VEC_2: [i32; 9] = [-10, 32, 12, -43, 52, -53, 83, -2, 9]; + const MIN_VEC: i32 = 2; + const ABS_MIN_VEC_2: i32 = -2; + + #[test] + fn test_find_minimum() { + let min: Option<i32> = find_minimum(&(VEC.to_vec())); + + assert!(min == Some(MIN_VEC)); + } + + #[test] + fn test_find_absolute_minimum() { + let min: Option<i32> = find_absolute_minimum(&(VEC_2.to_vec())); + + assert!(min == Some(ABS_MIN_VEC_2)); + } +} diff --git a/codes/rust_lang/part10/src/main.rs b/codes/rust_lang/part10/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..33df24f1b266a2e676e2a4f2cedb516132ab5c51 --- /dev/null +++ b/codes/rust_lang/part10/src/main.rs @@ -0,0 +1,26 @@ +use part10::find::{find_absolute_minimum, find_even_minimum, find_minimum}; +use part10::io; + +fn main() { + let v = io::read_command_line_correct(); + println!("Among the elements in the list:"); + io::print_vec(&v); + + let min = find_minimum(&v); + match min { + Some(val) => println!("The minimum value is {}", val), + None => eprintln!("There is no minimum"), + } + + let min = find_absolute_minimum(&v); + match min { + Some(val) => println!("The minimum by absolue value is {}", val), + None => eprintln!("There is no minimum"), + } + + let min = find_even_minimum(&v); + match min { + Some(val) => println!("The smallest even value is {}", val), + None => eprintln!("There is no minimum"), + } +} diff --git a/slides/src/SUMMARY.md b/slides/src/SUMMARY.md index 399cd777d11ee1d780ecfcfb6d2c34402f7ef7ca..cb42373b38d60a3b066327813fa487299fe532cb 100644 --- a/slides/src/SUMMARY.md +++ b/slides/src/SUMMARY.md @@ -2,6 +2,7 @@ - [Introduction](introduction.md) - [Installation](installation.md). +- [Itérateurs](iterators.md). - [Variables](variables.md). - [Types](types.md). - [Structures de contrôle](control.md). diff --git a/slides/src/iterators.md b/slides/src/iterators.md new file mode 100644 index 0000000000000000000000000000000000000000..d7dadb2cc90ed204bfe2f4910b07f30b7cb0a833 --- /dev/null +++ b/slides/src/iterators.md @@ -0,0 +1,143 @@ +# Les itérateurs + +## Le trait itérateur + +Le trait itérateur d'après la documentation + +```rust +trait Iterator { + type Item; + fn next(&mut self) -> Option<Self::Item>; +} +``` + +## Les itérateurs infinis + +```rust[7-11|13-15] +const fn my_random(s: i32) -> i32 { + // PRNG See wikipedia : https://en.wikipedia.org/wiki/Linear_congruential_generator + (((1664525i64 * s as i64) + 1013904223i64) % (1i64 << 32)) as i32 +} + +fn main() { + let mut seed: i32 = 123456; + let mut rand_iter = std::iter::from_fn(move || { + seed = my_random(seed); + Some(seed % 100) + }); + + println! {"{:?}", rand_iter.next()}; + println! {"{:?}", rand_iter.next()}; + println! {"{:?}", rand_iter.next()}; +} +``` + +## La fonction iter + +```rust[3-4|6-9] +fn main() +{ + let v = vec![0, 1, 2]; + let mut v_iter = v.iter(); + + println! {"{:?}", v_iter.next()}; + println! {"{:?}", v_iter.next()}; + println! {"{:?}", v_iter.next()}; + println! {"{:?}", v_iter.next()}; +} +``` + +## La fonction collect + +```rust[7-11|13|14] +const fn my_random(s: i32) -> i32 { + // PRNG See wikipedia : https://en.wikipedia.org/wiki/Linear_congruential_generator + (((1664525i64 * s as i64) + 1013904223i64) % (1i64 << 32)) as i32 +} + +fn main() { + let mut seed: i32 = 123456; + let mut rand_iter = std::iter::from_fn(move || { + seed = my_random(seed); + Some(seed % 100) + }); + + let v_rand: Vec<i32> = rand_iter.take(5).collect(); + println!("{:?}", v_rand); +} +``` +- La fonction `take(5)` crée un nouvel itérateur de 5 éléments. + +## `map()` + +```rust[5|] +fn main() +{ + let v = vec![0, 1, 2]; + let v_res: Vec<i32> = v.iter() + .map(|x| x + 1) + .collect(); + + println! {"{:?}", v_res}; +} +``` + +## `filter()` + +```rust[6|] +fn main() +{ + let v = vec![0, 1, 2]; + let v_res: Vec<i32> = v.iter() + .map(|x| x + 1) + .filter(|x| x % 2 == 1) + .collect(); + + println! {"{:?}", v_res}; +} +``` + +## Rappel élément neutre + +L'élément neutre `N` d'une opération `★` est l'élément tel que `N ★ x = x ★ N = x` pour tout `x`. + +- Exemple : 0 pour l'addition, car `0 + x = x + 0 = x` +- Exemple : 1 pour la multiplication, car `1 * x = x * 1 = x` + +## `fold()` + +```rust[8|4|] +fn main() +{ + let v = vec![0, 1, 2]; + let sum: i32 = v + .iter() + .map(|x| x + 1) + .filter(|x| x % 2 == 1) + .fold(0, |acc, current| acc + current); + + println! {"{:?}", sum}; +} +``` + +## `zip()` + +```rust[6-8|9-11|] +fn main() +{ + let chiffres = vec![1, 2, 3, 4]; + let lettres = vec!['a', 'b', 'c', 'd']; + + chiffres + .iter() + .zip(lettres.iter()) + .for_each( + |(chiffre, lettre)| println!("{lettre} est la lettre n°{chiffre} de l'alphabet",), + ); +} +``` + +## Lazy evaluation et performances + +- La performance des itérateurs est comparable aux boucles +- Les transformations sont appliquées uniquement lorsque que l'on veut récupérer une valeur. On appelle ça **lazy evaluation**