Vu tout ce à quoi peut servir python, pas facile de trouver un «nouveau» langage. J’ai jeté mon dévolu sur rust, histoire de voir s’il se passait enfin quelque chose d’intéressant côté langages compilés, vu les échecs totaux de C++, D, go et consorts. Je fais un grand sac, hein, je peux rajouter C# et Java si on me chauffe un peu. Mais là, je me tiens bien, les oreilles couchées, bien droit sur ma chaise, la raie de côté.

Et que dire si ce n’est que, apprendre le rust, ça me permet de prendre des coups de pelle plein fer à un rythme assez soutenu. Et pas que à cause des points-virgules ! Ne me lancez pas sur les points-virgules. S’il-vous-plaît.

Mon projouet : une classe «vecteur à deux dimensions»

Pour résumer : un machin qui contient un x et un y, qu’on peut additionner ou modifier par addition, pour utiliser sur codingame. En Python, ça donnerait ça :

class V2:

  def __init__(self, x, y):
    self.x = x
    self.y = y

  def __add__(self, autre):
    return V2(self.x + autre.x, self.y + autre.y)

Ce n’est pas avec ça qu’on révolutionnera les Internets, mais bon, il faut bien commencer quelque part. Pour l’histoire, mon premier projouet était un conteneur générique à références croisées ; force est de constater que j’avais été un peu trop ambitieux. Je n’ai plus fait de rust pendant six mois.

La gifle : choisir un type de base

Alors, hardi petit, et on tapote en rust :

struct V2 {
  x: usize,
  y: usize,
}

Le connaisseur en gestion de configuration appréciera la virgule à la troisième ligne.

- Ha ! Pourquoi usize et pas u64 ?

- Disons, l’expérience professionnelle.

- Oui mais, des fois on va faire des calculs dans tous les sens, on va perdre de la précision partout, c’est trop horrible. Faudrait faire une classe patron, comme ça je dirai en fonction des usages si j’ai besoin d’entiers ou de flottants.

- Tout doux, Bijou ! Les années 2000 sont terminées. Bataille pas, colle des flottants partout, on verra plus tard.

- Oui mais…

- Oui mais rien du tout, tu fais ce que je dis, on est la même personne, je te rappelle.

- Bon.

Alors, je m’obéis, et je change :

#[derive(Debug)]
struct V2 {
  x: f64,
  y: f64,
}

Bon. J’ai déjà envie d’une douche, mais je sais que ce genre de choix est inhérent au type du langage. il faut choisir. L’annotation initiale (« [derive(Debug)]« ), c’est pour pouvoir afficher le contenu de mes objets histoire de déboguer à la console comme un cochon, hein. Pas d’inquiétude. Ou presque.

La béquille : surcharger un opérateur arithmétique

Surcharger les opérateurs arithmétiques, mais pourquoi faire ? En premier lieu : du code lisible. Alors, je me paluche la documentation, et finis par tomber sur les traits «std::ops». C’est ça qu’il me faut ! Je tape :

impl std::ops::Add for V2 {
  type Output = V2;
  fn add(self, autre: V2) -> V2 {
    V2 { x: self.x + autre.x, y: self.y + autre.y }
  }
}

Bon, ça a l’air facile comme ça, mais une lecture attentive nous montre que ça ne va pas marcher. Si par exemple, j’utilise tout ça dans une belle fonction principale :

fn main() {
  let v1 : V2{ x: 15.0, y: -3.0 };
  let v2 : V2{ x: -4.0, y: 27.0 };
  let v3 = v1 + v2;
}

Jusqu’ici, tout va bien, mais si  je rajoute juste avant la fin :

  println!("{:?}", v1);

Et bien paf ! ça ne compile pas, et pourquoi ? Parce que la propriété de v1 a été transférée à la fonction d’addition, donc n’est plus disponible. C’est logique, c’est le langage qui veut ça. Pour que ça, ça marche, j’aurais du écrire :

  let v3 = &v1 + &v2;

C’est bien ce que je veux : utiliser v1 sans le modifier, pour pouvoir faire mon calcul. Du coup, l’opérateur que je dois implémenter n’est pas une addition à base de valeurs, mais de références. Et donc, en m’inspirant de BigInt, je rajoute, attention les yeux :

impl<'a, 'b> std::ops::Add<&'b V2> for &'a V2 {
  type Output = V2;

  fn add(self, autre: &V2) -> V2 {
    V2::new(self.x + autre.x,
            self.y + autre.y)
  }
}

Ça a l’air compliqué comme ça (bouh !), mais ça ne fait que l’addition de deux références aux cycles de vie différents. J’ai donc deux opérations d’addition :

- une entre deux valeurs (qui seront donc définitivement perdues ensuite) ;

- une entre deux références.

Je pourrais aussi avoir besoin d’autres variantes pour les fois où je ferais des trucs plus compliqués, comme additionner trois vecteurs sur une seule ligne : une addition entre une valeur et une référence, et entre une référence et une valeur. Soit deux surcharges supplémentaires :

// &v1 + v2
impl<'a> std::ops::Add<V2> for &'a V2 {
  type Output = V2;

  fn add(self, autre: V2) -> V2 {
    V2 { x:self.x + autre.x,
         y: self.y + autre.y }
  }
}

// v1 + &v2
impl<'b> std::ops::Add<&'b V2> for V2 {
  type Output = V2;

  fn add(self, autre:&V2) -> V2 {
    V2 { x: self.x + autre.x,
         y: self.y + autre.y}
  }
}

Ouf ! Ça commence à faire des kilomètres de code pour pas grand chose… La robustesse du logiciel a un prix : le voici ! Sans compter qu’on a l’impression de retomber dans le C++, ambiance référence sur objet temporaire et compagnie… Il doit y avoir moyen de faire plus simple, parce que sinon, quand il faudra rajouter les autres opérations, je vais décéder ! J’ai cru voir tout un système de macros, on ira voir plus tard.

La variante : le poisson chinois

Je viens tout de même d’écrire quatre variantes pour un même opérateur, soit quatre sources de bogues assez subtiles, opérateur que j’ai favorisé sur une banale méthode pour écrire «du code lisible», et j’en viens à devoir écrire «v3 = &v1 + &v2» pour écrire du code correct et sûr. Comme une impression d’avoir laché la proie pour l’ombre, pas vous ? Mais il y a une autre option : le trait «std::marker::Copy». Qu’est-ce ? Si je suis près à faire une croix sur l’unicité des références aux instances, tout est plus simple. En dérivant ce trait, seule la première implémentation naïve de l’addition suffit.

Fort bien, mais qu’est-ce que le trait Copy ? De ce que j’en comprends, une directive de compilation (puisqu’il n’y a rien à implémenter de spécifique) pour dire que toutes les copies physiques d’une instance sont équivalentes. Est-ce que du coup le compilateur produit effectivement des copies quand j’écris «v3 = v1 + v2» ? Mystère. Je sens que ce simple trait est un sujet de thèse à part entière. Alors j’assume : je ne le dérive pas, et j’implémente mes quatre variantes de la même addition en attendant d’en savoir plus.

Un coude, une cloison nasale : deux raisons de saigner

Maintenant qu’on titube de douleur, on se dit : «Tiens, et si je mettais mes vecteurs dans des conteneurs ? Ça pourrait arriver, dans la vraie vie.» Ni une, ni une, je tapote :

let mut s = std::collections::HashSet::new();
s.insert(v3);
println!("{}", s.len());

Las ! Je reprends une mandale : comment voulez-vous distinguer deux instances si vous ne dites pas comment les hacher ? Et le trait «std::hash::Hash» alors ? Bon, je ne peux pas franchement donner tort au compilateur. Allons au plus vite : dérivons le trait standard :

#[derive(Hash)]

Le compilateur n’en veut pas : le type «f64» n’est pas hachable par défaut. J’imagine que c’est à cause de la norme IEEE-754 et de ses classes d’équivalence. J’ai donc le choix entre écrire comment hacher un «f64», mais ça risque d’être valable pour tout le programme, soit limiter ma façon de hacher du f64 aux bornes de mon vecteur. Je choisis cette dernière façon de pourrir le code, et j’implémente explicitement le trait std::hash::Hash plutôt que de le dériver :

impl std::hash::Hash for V2 {
  fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
    self.x.to_bits().hash(state);
    self.y.to_bits().hash(state);
  }
}

En passant par la représentation physique (avec «f64::to_bits», j’obtiens un entier parfaitement hachable ! C’est moche mais c’est isolé, alors ce n’est pas grave. Bon, ça passe presque : maintenant qu’on sait faire de la bouillie de vecteur, il faut aussi introduire une relation d’équivalence (le trait std::cmp::Eq). Ce n’est pas moi, hein, c’est le compilateur, n’oubliez pas. Histoire d’assurer que deux instances équivalentes aient même haché (cf. ). N’imaginez même pas dériver du trait Eq ! J’imagine que c’est pour les mêmes raisons que f64 n’est pas hachable. Par contre, on peut au moins dériver le trait std::cmp::PartialEq, ce qui est déjà pas mal. Bref, il ne reste qu’à rajouter :

#[derive(PartialEq)]
…
impl std::cmp::Eq for V2 {
}

Et là, enfin ! Ça compile, ça se lance, ça ne fait pas n’importe quoi ! Il serait temps me direz-vous. Je peux même insérer deux fois v3, ça passe… Enfin, deux références à v3, bien sûr.

[…à suivre…]