Rust

Any donation is very welcome
Fork me on GitHub

II. Spécificités de Rust

4. Généricité

Reprenons donc notre précédent exemple :

Runfn affiche_chat(chat: &Chat) -> String {
    println!("{} est un {}", chat.get_nom(), chat.get_espece());
}

fn affiche_chien(chien: &Chien) -> String {
    println!("{} est un {}", chien.get_nom(), chien.get_espece());
}

Comme je vous le disais, avec deux espèces d'animaux, ça ne représente que 2 fonctions mais ça deviendra très vite long à écrire si on veut en rajouter 40. C'est donc ici qu'intervient la généricité.

La généricité

Commençons par la base en donnant une description de ce que c'est : "c'est une fonctionnalité qui autorise le polymorphisme paramétrique (ou juste polymorphisme pour aller plus vite)". Pour faire simple, ça permet de manipuler des objets différents du moment qu'ils implémentent le trait demandé.

Par exemple, on pourrait manipuler un chien robot, il implémenterait le trait Machine et le trait Animal :

Runtrait Machine {
    fn get_nombre_de_vis(&self) -> u32;
    fn get_numero_de_serie(&self) -> &str;
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> u32;
}

struct ChienRobot {
    nom: String,
    nombre_de_pattes: u32,
    numero_de_serie: String,
}

impl Animal for ChienRobot {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

impl Machine for ChienRobot {
    fn get_nombre_de_vis(&self) -> u32 {
        40123
    }

    fn get_numero_de_serie(&self) -> &str {
        &self.numero_de_serie
    }
}

Ainsi, il nous est désormais possible de faire :

Runfn presentation_animal<T: Animal>(animal: T) {
    println!("Il s'appelle {} et il a {} patte()s !",
             animal.get_nom(),
             animal.get_nombre_de_pattes());
}

let super_chien = ChienRobot {
                      nom: "Super chien".to_owned(),
                      nombre_de_pattes: 4,
                      numero_de_serie: String::from("super chien DZ442"),
                  };

presentation_animal(super_chien);

Mais comme c'est aussi une machine, on peut aussi faire :

Runfn description_machine<T: Machine>(machine: T) {
    println!("Le modèle {} a {} vis",
             machine.get_numero_de_serie(),
             machine.get_nombre_de_vis());
}

C'est pas trop génial ?

Revenons-en maintenant à notre problème initial : "comment faire avec 40 espèces d'animaux différentes" ? Je pense que vous commencez à voir où je veux en venir je présume ? Non ? Très bien, dans ce cas prenons un autre exemple :

Runtrait Animal {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

struct Chien {
    nom: String,
    nombre_de_pattes: u32,
}

struct Chat {
    nom: String,
    nombre_de_pattes: u32,
}

struct Oiseau {
    nom: String,
    nombre_de_pattes: u32,
}

struct Araignee {
    nom: String,
    nombre_de_pattes: u32,
}

impl Animal for Chien {}
impl Animal for Chat {}
impl Animal for Oiseau {}
impl Animal for Araignee {}

fn affiche_animal<T: Animal>(animal: T) {
    println!("Cet animal s'appelle {} et il a {} patte(s)",
             animal.get_nom(),
             animal.get_nombre_de_pattes());
}

let chat = Chat { nom: String::from("Félix"), nombre_de_pattes: 4 };
let spider = Araignee { nom: String::from("Yuuuurk"), nombre_de_pattes: 8 };

affiche_animal(chat);
affiche_animal(spider);

Pas mal hein ? Et pourtant... Ce code ne compile pas !

Pourquoi ?!

Tout simplement parce que les traits ne peuvent prendre en compte, dans les méthodes par défaut, le travail sur des valeurs contenues dans l'objet pris en paramètre (référencé dans cet exemple par self).

Imaginez une baleine et un chien. Tous deux sont des animaux, pas vrai ? Pourtant, ils ont très peu de points communs...

Ainsi, chacun aura des caractéristiques que l'autre pourrait ne pas avoir (la couleur des poils par exemple). Programmer une fonction par défaut sur un attribut qu'un objet implémentant le trait ne possède pas pourrait poser problème !

Voici un code fonctionnant pour ce cas :

Runstruct Chien {
    nom: String,
    nombre_de_pattes: u32,
}

struct Chat {
    nom: String,
    nombre_de_pattes: u32,
}

trait Animal {
    fn get_nom(&self) -> &str;
    fn get_nombre_de_pattes(&self) -> u32;
    fn affiche(&self) {
        println!("Je suis un animal qui s'appelle {} et j'ai {} pattes !",
                 self.get_nom(),
                 self.get_nombre_de_pattes());
    }
}

// On implémente les méthodes prévues dans le trait Animal, sauf celles par défaut
impl Animal for Chien {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }
}

// On fait de même, mais on a quand même envie de surcharger la méthode par défaut...
impl Animal for Chat {
    fn get_nom(&self) -> &str {
        &self.nom
    }

    fn get_nombre_de_pattes(&self) -> u32 {
        self.nombre_de_pattes
    }

    // On peut même 'surcharger' une méthode par défaut dans le trait - il suffit de la réimplémenter
    fn affiche(&self) {
        println!("Je suis un animal - un chat même qui s'appelle {} !", self.get_nom());
    }
}

fn main() {
    fn affiche_animal<T: Animal>(animal: T) {
        animal.affiche();
    }

    let chat = Chat { nom: "Félix".to_owned(), nombre_de_pattes: 4};
    let chien = Chien { nom: "Rufus".to_owned(), nombre_de_pattes: 4};

    affiche_animal(chat);
    affiche_animal(chien);
}

La seule contrainte étant que, même si l'implémentation des méthodes est la même, il faudra, à chaque structure héritant d'un trait, la réimplémenter... Cela dit, les macros pourraient grandement faciliter cette étape laborieuse, mais nous verrons cela plus tard.

Where

Il est aussi possible d'écrire un type/une fonction générique en utilisant le mot-clé where :

Runfn affiche_animal<T>(animal: T)
    where T: Animal {
    println!("Cet animal s'appelle {} et il a {} patte(s)",
             animal.get_nom(),
             animal.get_nombre_de_pattes());
}

Dans l'exemple précédent, cela n'apporte strictement rien. Cependant, where permet d'ajouter des clauses à la généricité et est plus lisible sur les fonctions/types prenant beaucoup de paramètres génériques :

Runuse std::fmt::Debug;

trait UnTrait<A, B> {
    fn copy_a(&mut self, &A);
    fn clone_b(&mut self, &B);
}

struct UneStruct<A, B> {
    a: A,
    b: B,
}

impl<A, B> UnTrait<A, B> for UneStruct<A, B>
    where A: Copy + Debug,
          B: Clone {
    fn copy_a(&mut self, a: &A) {
        self.a = *a;
        println!("copy_a: {:?}", a);
    }

    fn clone_b(&mut self, b: &B) {
        self.b = b.clone();
    }
}