Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

13. Les itérateurs

Un problème couramment rencontré par les débutants en Rust semble être l'implémentation du trait Iterator. Nous allons donc tenter de remédier à cela en expliquant comme ils fonctionnent.

Jusqu'ici, nous savons qu'il existe deux types d'Iterators :

  • Les itérateurs sur/liés à un type.
  • Les générateurs.

Les itérateurs sur/liés à un type

Cette structure va itérer sur un ensemble de données. Bien qu'elle reste la plus complexe des deux (à cause des durées de vie notamment), sa mise en place n'a rien d'insurmontable. Prenons un exemple :

Imaginons que vous ayez besoin de wrapper un Vec tout en ayant
la capacité d'itérer sur le type fraîchement créé pour l'occasion.

Définissons la structure proprement dite :

Runstruct NewType<T>(Vec<T>);

Nous allons, maintenant, avoir besoin d'implémenter le trait Iterator. Le principal problème est que vous ne pouvez pas stocker un paramètre dans la
structure NewType qui pourrait vous permettre de suivre la progression de la lecture à l'intérieur de votre vecteur et... c'est ici que la plupart des gens sont perdus. La solution est en réalité plutôt simple :

Run// On crée une nouvelle structure qui contiendra une référence de votre ensemble
// de données.
struct IterNewType<'a, T: 'a> {
    inner: &'a NewType<T>,
    // Ici, nous utiliserons `pos` pour suivre la progression de notre itération.
    pos: usize,
}

// Il ne nous reste plus alors qu'à implémenter le trait `Iterator` pour `IterNewType`.
impl<'a, T> Iterator for IterNewType<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        if self.pos >= self.inner.0.len() {
            // Il n'y a plus de données à lire, on stoppe l'itération.
            None
        } else {
            // On incrémente la position de notre itérateur.
            self.pos += 1;
            // On renvoie la valeur courante pointée par notre itérateur.
            self.inner.0.get(self.pos - 1)
        }
    }
}

Simple, non ? Il nous reste plus qu'à ajouter la méthode iter à notre structure NewType :

Runimpl<T> NewType<T> {
    fn iter<'a>(&'a self) -> IterNewType<'a, T> {
        IterNewType {
            inner: self,
            pos: 0,
        }
    }
}

Fini !

Voici un petit exemple d'utilisation de notre structure :

Runfor x in NewType(vec![1, 3, 5, 8]).iter() {
    println!("=> {}", x);
}

Résultat :

=> 1
=> 3
=> 5
=> 8

Les générateurs

Un générateur est une manière plutôt intéressante (et simple) d'utiliser les Iterators en Rust. Un exemple sera certainement plus parlant dans ce cas précis :

Run// Notre structure itère (on peut aussi dire "génère) uniquement sur les
// nombres impairs.
struct Impair {
    current: usize,
}

impl Impair {
    fn new() -> Impair {
        Impair {
            // La première valeur impaire positive est 1, donc commençons à 1.
            current: 1,
        }
    }
}

impl Iterator for Impair {
    type Item = usize;

    fn next(&mut self) -> Option<Self::Item> {
        // Déplaçons-nous à la valeur impaire suivante.
        self.current += 2;
        // On renvoie la valeur impaire courante.
        Some(self.current - 2)
    }
}

fn main() {
    // Pour éviter de boucler indéfiniment avec notre itérateur `Impair`, nous
    // avons limité la boucle à 3 valeurs.
    for x in Impair::new().take(3) {
        println!("=> {}", x);
    }
}

Résultat :

=> 1
=> 3
=> 5

Comme vous pouvez le constater, Impair génère ses propres valeurs, contrairement à l'exemple
précédent qui était basé sur celles d'un vecteur. Sa conception rend la génération infinie, mais
il est tout à fait possible d'établir une limite (aussi bien interne à la structure que dans son
utilisation). À vous de voir selon vos besoins !

Par-exemple, si on créait un itérateur sur des nombres premiers, il ne pourrait continuer
que jusqu'au dernier nombre premier connu.

Conclusion

Les itérateurs peuvent se montrer puissants et restent relativement simples à implémenter en Rust, mais
les débutants ont tendance à directement gérer la ressource et itérer dessus, ce qui complique
généralement la recherche de solutions potentiellement plus adaptées.

Il est toujours question de penser "Rust" ou non !

Article original

Ce chapitre a été écrit à partir de cet article de blog. N'hésitez pas à y faire un tour !