- Explique une approche de conception Rust qui utilise le système de types pour garantir les invariants à la compilation au lieu de valider à l’exécution
- Définit de nouveaux types (newtypes) comme
NonZeroF32 et NonEmptyVec afin de rendre impossibles les états invalides (0, vecteur vide, etc.)
- Au lieu de renvoyer un échec via
Option ou Result, l’idée est de renforcer les contraintes dès les arguments des fonctions pour bloquer les erreurs en amont
- Présente des exemples comme
String::from_utf8 ou serde_json::from_str, où le parsing transforme des données en types porteurs de sens
- Le principe de conception consistant à rendre les états illégaux impossibles à représenter et à avancer la validation le plus tôt possible améliore la robustesse et la lisibilité du code
1. Exprimer les contraintes par les types plutôt que par la validation à l’exécution
- Dans une fonction
divide(a, b), une division par 0 provoque un panic à l’exécution
- On peut représenter l’échec en renvoyant une
Option, mais cela revient à affaiblir le type de retour
- En définissant un type
NonZeroF32, on permet de créer uniquement des valeurs non nulles
- Le constructeur a la forme
fn new(n: f32) -> Option<NonZeroF32>, et renvoie None en cas d’échec
- Si l’on définit
divide_floats(a: f32, b: NonZeroF32), la validation à l’exécution devient inutile
- La responsabilité de la validation est ainsi déplacée de l’intérieur de la fonction vers l’appelant, ce qui élimine les erreurs en amont
2. Suppression des validations redondantes et simplification du code
- Dans une fonction
roots(a, b, c), si la vérification a == 0 est gérée avec une Option, cela entraîne des validations redondantes à la fois chez l’appelant et dans la fonction
- Avec
NonZeroF32, la validation n’est effectuée qu’une seule fois, puis la logique suivante est simplifiée
- Le même principe s’applique à
NonEmptyVec<T>, qui interdit les vecteurs vides
- Si
get_cfg_dirs() renvoie NonEmptyVec<PathBuf>, aucune validation supplémentaire n’est nécessaire ensuite dans main()
3. Cas concrets : String et serde_json
String est en interne un nouveau type (newtype) autour de Vec<u8>, et String::from_utf8 effectue la vérification de validité
- Ensuite, on peut l’utiliser en toute sécurité comme une chaîne garantie UTF-8
- Avec
serde_json, from_str::<Sample> parse le JSON en structure et garantit au moment de la compilation l’existence des champs et la cohérence des types
- La présence des champs
foo et bar, la correspondance des types, la longueur des tableaux et les autres contraintes sont ainsi vérifiées au niveau des types
4. Deux principes de la conception guidée par les types
- Rendre les états illégaux impossibles à représenter
NonZeroF32 ne peut pas représenter 0, et NonEmptyVec ne peut pas représenter un état vide
- Une simple fonction de validation (
is_nonzero) reste incomplète, car elle laisse encore exister des états invalides
- Effectuer la validation le plus tôt possible
- Si, comme dans le « Shotgun Parsing », la validation est dispersée dans tout le code, cela peut mener à des vulnérabilités de sécurité (comme CVE-2016-0752)
- Si toutes les contraintes sont vérifiées au moment du parsing, la logique suivante peut s’exécuter en toute sûreté
5. Preuves par les types et applications en Rust
- Selon la correspondance de Curry-Howard, les types peuvent être vus comme des propositions logiques, et les valeurs comme leurs preuves
- Avec la crate
typenum, il est possible de vérifier à la compilation des relations mathématiques (3 + 4 = 8)
- Le système de types permet ainsi de prouver la correction d’un programme dès la compilation
6. Conseils d’application en pratique
- Même si une API externe exige des types simples (
bool, i32), il vaut mieux représenter cela en interne avec des enum ou des newtypes porteurs de sens
- Exemple : définir
LightBulbState { On, Off } puis implémenter From<LightBulbState> for bool
- Si vous avez des fonctions comme
verify() ou do_something_fallible(), il faut envisager une conversion en types structurés via le parsing plutôt qu’une simple validation
- Pour une fonction sans effet de bord, on peut aussi utiliser
Result<Infallible, MyError> pour exprimer délibérément un état impossible dans le type
7. Conclusion
- En Rust, utiliser le système de types comme outil de validation améliore la clarté et la robustesse du code
- Plusieurs outils de l’écosystème Rust, comme
Vec, sqlx et bon, s’appuient déjà sur une conception fondée sur les types
- Tous les problèmes ne peuvent pas être résolus uniquement par les types, mais faire remonter la logique de validation au niveau des types améliore la maintenabilité et la sécurité
- Il est recommandé d’exploiter au maximum la puissance du système de types de Rust afin d’écrire du code où le compilateur intercepte les erreurs
Aucun commentaire pour le moment.