Rust : jouer avec le typage fort
Pierre qui rouille… attendez, quoi ?!
J’ai récemment appris Rust. Parce que le langage est marrant avec son concept d’appartenance (ownership), sa gestion des erreurs très intelligentes, ses pointeurs intelligents (smart pointers). Parce que j’aime bien apprendre des choses.
Et surtout parce que quand le shell n’est plus suffisant, passer en python avait une certain tendance à me casser les noix : il faut des librairies installées ou il faut distribuer de grosses librairies avec certains programmes ou scripts alors qu’on peut simplement tout embarquer dans un exécutable Rust si on en a besoin.
Bref, c’est cool.
Toutefois, Rust reste un langage fortement typé et quand on a l’habitude des langages faiblement typés (PHP, Python, etc…), cela peut nécessiter un sacré temps d’adaptation pour s’y remettre. Soyons clairs : j’aime bien les langages fortement typés. Ça donne l’impression de savoir ce que l’on fait et ça permet d’éliminer un nombre substantiels de causes d’erreur. Mais c’est aussi pas évident du tout à manipuler et ça prend nettement plus de temps pour arriver au même résultat qu’un langage faiblement typé.
Je n’ai pas la prétention de vous apprendre à programmer en Rust. Même si j’ai commencé à faire des choses bien plus compliquées dans ce langage, j’ai débuté comme tout le monde. Et au moment de débuter, on bute toujours sur des choses stupides, comme ce que je vais vous présenter là et qui a, du coup, un rapport avec une particularité du langage, le typage fort.
Si vous êtes débutant en Rust, ou si vous ne connaissez pas Rust et que vous souhaitez l’apprendre, ceci pourrait vous intéresser.
Alors la marmotte, elle met le chocolat dans un entier 64 bits non-signé
Prenons un exemple tout con. On va faire une fonction dont le seul objectif est d’afficher un nombre qui est donné en paramètre. Cette fonction pourrait s’écrire comme ça :
fn print_number(a: u64) {
println!("{}", a);
}
Tu peux alors l’utiliser de manières assez simple :
fn main() {
let a: u64 = 8;
print_number(a);
}
Basiquement, ça va fonctionner, mais cela pose quand même un gros souci : on ne peut passer à cette fonction que des u64
. Or, a
dans notre exemple pourrait très bien tenir dans un u8
. Mais si l’on fait, ça, ça ne fonctionnera pas :
fn main() {
let a: u8 = 8;
print_number(a);
}
error[E0308]: mismatched types
--> src/main.rs:9:18
|
9 | print_number(a);
| ^
| |
| expected u64, found u8
| help: you can convert an `u8` to `u64`: `a.into()`
Du point de vue de Rust, c’est hyper cohérent : on lui dit qu’une fonction prend en paramètre un u64
, on essaie de lui passer un u8
, le compilateur gueule parce que ce n’est pas normal. Ce n’est pas le même type. Et même si dans l’absolu un u8
rentre parfaitement dans un u64
, Rust s’en moque : un type est un type et un type « compatible » reste un type différent.
Alors évidemment, tu peux tenter de caster systématiquement ta variable dans un u64
(c’est ce que propose le compilateur en fait). Pour cela, tu pourrais appeler la fonction de cette manière :
print_number(a as u64);
Ça règle effectivement le problème mais :
- ce n’est pas forcément très élégant
- tu vas te retrouver à jongler avec des types alors que finalement, ça ne t’intéresse pas des masses
Tu pourrais aussi imaginer de faire une fonction pour chaque type : u64
, u32
, u16
, etc… mais franchement, ce serait hyper fastidieux.
Du coup, comment on fait ?
Tu peux aller t’implémenter Martine !!
En fait, on peut très facilement rendre cette fonction beaucoup plus universelle en remplaçant le type fixe par un ''Trait''. L’idée est ici de dire : finalement vu ce que je fais dans la fonction, ce qui m’intéresse, c’est que ce qui est passé en paramètre puisse s’afficher dans un println!
pas tellement le fait que ce soit un nombre.
Et du coup, on peut réécrire cette fonction de la manière suivante :
fn print_number(a: impl std::fmt::Display) {
println!("{}", a);
}
À partir de ce moment, quelque soit ce qu’on passe dans la fonction (et même pas forcément un nombre !), tant que ce type implémente la fonction fmt::Display
, ça fonctionnera !
Du coup, tu peux très bien imaginer de faire nawak :
fn main() {
let a: u8 = 8;
let b: u16 = 16;
let c: f64 = 3.1416;
print_number(a);
print_number(b);
print_number(c);
}
Conclusage
Je voulais juste donner un tout petit exemple sur une problématique qui paraît hyper simple dans un langage faiblement typé, mais qui se révèle bien plus compliqué dès que le langage est fortement typé. Quand on commence à apprendre Rust, c’est typiquement le genre de choses sur lequel on bute bêtement au départ parce que ce n’est pas vraiment intuitif (en tout cas, pas tant qu’on a pas lu le chapitre sur le sujet dans le Book).
Quand on vient du C ou du PHP, c’est le type de problématique qui paraît insurmontable au départ alors qu’en réalité, c’est tout bête à résoudre. Et je trouve personnellement que Rust a une méthode particulièrement élégante pour le résoudre en prime et évidemment, on peut extrapoler cette méthode pour se donner bien plus de possibilités.