3 points par GN⁺ 2026-02-23 | 1 commentaires | Partager sur WhatsApp
  • 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

1 commentaires

 
GN⁺ 2026-02-23
Avis sur Hacker News
  • L’exemple de la division par zéro utilisé dans cet article n’est pas très adapté pour expliquer le principe « Parse, Don’t Validate »
    Le cœur de ce principe réside dans une fonction qui transforme des données non fiables en un type structurellement correct
    L’article d’Alexis King "Names are not type safety" explique lui aussi que le pattern newtype ne garantit pas un « correct by construction » complet
    Quand le système de types ne peut pas exprimer directement les invariants, une approche réaliste consiste à utiliser des types abstraits avec des constructeurs intelligents (smart constructors) qui imitent le comportement d’un parseur
    Le deuxième exemple, celui d’un vec non vide, est bien meilleur, car il garantit dans le système de types qu’« il y a toujours au moins un élément »

    • Même un « parse, don’t validate » basé sur newtype reste en pratique très utile
      Lorsqu’on ne sait pas d’où vient une chaîne, une valeur encapsulée augmente fortement la fiabilité
      Pour obtenir un correctness-by-construction complet, il faut un système de types dépendants, mais Rust a aussi des alternatives légères comme les pattern types
      On peut par exemple restreindre une plage avec i8 is 0..100, ou exprimer une slice non vide avec [T] is [_, ..]
      En revanche, une liste non vide sous la forme (T, Vec<T>) illustre le conflit entre praticité et pureté théorique, car elle impose beaucoup de contraintes si on veut la manipuler comme un vecteur
    • Le « correct by construction » est l’objectif ultime
      Des types comme NonZeroU32 sont simples, mais la vraie force consiste à concevoir toute la logique métier dans les types afin que le compilateur joue le rôle de gardien
      Cela déplace la charge du débogage de l’exécution vers la phase de conception
    • On peut aussi trouver des ressources connexes avec le mot-clé « make invalid states impossible/unrepresentable »
      Par exemple "Domain Modeling Made Functional" et la vidéo associée valent le détour
    • L’exemple de la division par zéro est un mauvais cas de séparation des préoccupations
      Au lieu d’essayer de l’enrober à ce niveau, il serait plus clair d’encapsuler le comportement de fonctions arithmétiques comme le dépassement de capacité
  • J’ai regroupé quelques liens vers des discussions récentes sur le sujet
    Parse, Don't Validate (2019) (février 2026, 172 commentaires)
    Parse, Don’t Validate – Some C Safety Tips (juillet 2025, 73 commentaires)
    Parse, Don't Validate (2019) (juillet 2024, 102 commentaires), entre autres
    C’est partagé simplement à titre de référence

  • L’approche parsing over validation a ses limites quand on ne peut pas connaître tous les cas du monde réel
    Faire échouer le plus tôt possible est une bonne chose pour des formats de fichiers, mais il faut être prudent lorsqu’on l’applique à la logique métier ou à la modélisation des transitions d’état
    Si les exigences du réel changent, le système peut ne plus réussir à les absorber, et les utilisateurs finissent alors par contourner les règles

  • Dans d’autres langages, on peut aller plus loin avec les types dépendants (dependent typing)
    Par exemple, get_elem_at_index(array, index) peut garantir à la compilation que l’index est dans les bornes, même sans connaître à l’avance la longueur du tableau
    Les types Vect n a et Fin n d’Idris en sont un exemple

    • En Rust aussi, il existe des bibliothèques qui imitent les types dépendants via des macros
      Exemple : anodized (vidéo de présentation)
    • Si la longueur du tableau est lue depuis stdin, elle n’est pas connue à la compilation, donc ce genre de vérification reste limité aux cas où une information statique existe
    • On aimerait voir ce genre de capacité se généraliser davantage
  • Il existe aussi une approche consistant à mettre plusieurs fonctions autour d’un seul type
    Comme en Clojure, où toutes les données sont représentées par une map unique, et où l’ensemble de la bibliothèque standard peut les manipuler

    • Il existe une tension entre la formule de Perlis, « 100 fonctions sur une seule structure de données », et « Parse, Don’t Validate »
      On peut intégrer les invariants importants dans les types, ou bien les exprimer par de simples fonctions
      Même dans les langages dynamiquement typés, il existe des habitudes de conception qui produisent un effet similaire
    • Ce n’est pas tant une alternative pure qu’un trade-off
      Les entrées externes doivent de toute façon finir par être parsées, donc cela ne les remplace pas complètement
    • Cela peut faire penser à la critique des « stringly typed languages », mais en pratique il s’agit d’un processus de raffinement progressif de la forme des données
    • L’équilibre est important
      Dans un système de types structurel, on peut imiter les types nominatifs avec du branding, et l’inverse est aussi possible, mais ce n’est pas très ergonomique
      En pratique, le plus réaliste est de mélanger les deux approches de manière appropriée
  • Cette discussion fait penser à la fonctionnalité concepts de C++
    Dans Concept-based Generic Programming de Bjarne Stroustrup, on montre par exemple une validation automatique des conversions entières
    Des types comme Number<unsigned int> ou Number<char> lèvent une exception si la valeur sort de l’intervalle autorisé

  • L’exemple try_roots de l’article est en fait un contre-exemple
    En Rust, exprimer dans les types la contrainte b^2 - 4ac >= 0 devient très complexe
    Dans ce genre de cas, il est plus raisonnable de simplement renvoyer une Option et de faire la validation dans la fonction
    La plupart des validations portent sur l’interaction entre plusieurs valeurs, ce qui les rend peu commodes à résoudre par du « parsing »

    • Quand la validité d’une entrée dépend de la relation entre plusieurs arguments, il faut en fin de compte les regrouper sous une forme comme fn(abc: ValidABC)
  • Ce pattern convient aussi très bien à la conception d’API
    Au lieu de valider une requête JSON, on peut dès le départ la parser dans une struct dont les types garantissent la validité, ce qui évite ensuite les validations redondantes dans la logique métier
    C’est facile à implémenter en Rust avec la combinaison serde + custom deserializer
    J’ai effectivement vu un cas où cette approche a réduit de 60 % le code de gestion d’erreurs

    • On essaie aussi en Go, mais cela devient assez verbeux à cause de l’abus de pointeurs et de l’absence de types algébriques
  • J’applique aussi la même philosophie aux design systems d’interface
    Au lieu d’inspecter le CSS après coup, on définit des types qui n’autorisent le placement que par unités de grille, de sorte qu’une marge arbitraire comme 13px devienne une erreur de compilation
    Cela permet de garder un design déterministe

    • Quelqu’un demandait quels outils étaient utilisés
  • Les records + pattern matching de C# s’approchent de cette idée
    Les discriminated unions de F# sont encore plus puissantes, car elles permettent avec Result<'T,'Error> de rendre les états invalides impossibles à représenter
    C# deviendra sans doute beaucoup plus élégant si des DU natives y sont introduites à l’avenir