Parser, ne validez pas
L’essence de la conception pilotée par les types
- Slogan simple pour expliquer la conception pilotée par les types (type-driven design) : parser, ne pas valider
- Ce slogan désigne une manière d’utiliser le système de types pour améliorer la sûreté et la justesse du code
Le domaine du possible
- Un système de types statique permet de déterminer facilement si une certaine fonction peut être implémentée ou non
- Exemple :
foo :: Integer -> Void est impossible à implémenter (Void ne peut contenir aucune valeur)
- Exemple : la fonction
head :: [a] -> a n’est pas définie lorsque la liste est vide
Transformer une fonction partielle en fonction totale
Gérer les attentes
- Comme la fonction
head ne peut pas renvoyer de valeur lorsque la liste est vide, on peut utiliser le type Maybe pour permettre le retour de Nothing
- Mais cela peut rendre son utilisation moins pratique
Transmettre les attentes
- En utilisant le type
NonEmpty pour représenter une liste non vide, on peut garantir que la fonction head renvoie toujours une valeur
- Le type
NonEmpty permet de supprimer des vérifications inutiles et d’attraper les erreurs à la compilation via le système de types
La puissance du parsing
- La différence entre parsing et validation tient à la manière dont l’information est préservée
- La fonction
validateNonEmpty vérifie qu’une liste n’est pas vide, mais ne préserve pas cette information
- La fonction
parseNonEmpty vérifie qu’une liste n’est pas vide et préserve cette information via le type NonEmpty
Les risques de la validation
- Une approche fondée sur la validation peut entraîner un problème appelé « shotgun parsing »
- Cela peut conduire à des situations où le programme traite une partie de l’entrée avant de découvrir que le reste n’est pas valide
- Le parsing divise le programme en deux étapes : la première vérifie la validité des entrées, la seconde ne traite que des entrées valides
Le parsing en pratique
- Se concentrer sur les types de données et rendre les signatures de type des fonctions aussi concrètes que possible
- Utiliser des structures de données qui ne peuvent pas représenter des états illégaux, et convertir les données vers une représentation concrète le plus tôt possible
- Laisser les types de données guider le code, plutôt que laisser le code contrôler les types de données
- Les fonctions qui renvoient
m () doivent être utilisées avec prudence
- Il ne faut pas avoir peur de parser les données en plusieurs fois
- Éviter les représentations dénormalisées des données et, si nécessaire, les gérer via l’encapsulation
- Utiliser des types de données abstraits qui donnent aux validateurs l’apparence de parseurs
Résumé, réflexion et lectures associées
- Exploiter au maximum le système de types de Haskell n’est pas difficile et ne nécessite pas d’utiliser les extensions de langage les plus récentes
- L’idée centrale est d’« écrire des fonctions totales », ce qui est simple en théorie mais peut être difficile à mettre en pratique
- Parmi les lectures associées, l’article de blog de Matt Parson « Type Safety Back and Forth » et le papier de Matt Noonan « Ghosts of Departed Proofs » sont recommandés
Le résumé de GN⁺
- Cet article explique comment utiliser le système de types de Haskell pour améliorer la sûreté et la justesse du code
- Il souligne l’importance de comprendre la différence entre parsing et validation, et de vérifier la validité des entrées via le parsing
- Il est important d’utiliser le système de types pour employer des structures de données qui ne peuvent pas représenter des états illégaux, et de convertir les données vers une représentation concrète le plus tôt possible
- Parmi les lectures associées, l’article de blog de Matt Parson et le papier de Matt Noonan sont recommandés
1 commentaires
Discussion sur Hacker News
Ce conseil et cet article sont très instructifs
C’est utile même pour les personnes qui n’utilisent pas de langage fonctionnel à typage statique
Cette idée dépasse les paradigmes
On peut trouver des concepts similaires dans la littérature orientée objet des années 80 et 90, par exemple Design by Contract
TypeScript est souvent écrit de manière à affiner les types à l’exécution
Design by Contract a probablement influencé
specde Clojure (Clojure est un langage dynamique)Fondamentalement, il s’agit d’hypothèses et de garanties (exiger et fournir)
Une fois les hypothèses vérifiées et les garanties établies, il n’est plus nécessaire de revérifier des hypothèses redondantes dans d’autres parties du programme
Voir dans le code des propriétés déjà garanties être vérifiées à nouveau peut être déroutant, ce qui complique la compréhension et l’amélioration du code
Ce pattern fonctionne aussi très bien en C# moderne et permet également d’économiser de l’espace
Il est préférable d’exploiter un système de types puissant pour rendre les cas d’erreur impossibles à représenter, ce qui aide à réduire les bugs logiciels
Cela demande plus de temps pour réfléchir au problème et suivre la conception, mais dans bien des cas ce temps en vaut la peine
Le slogan « Parse, don’t validate » résume bien la conception fondée sur les types
Personnellement, je pense qu’il vaut mieux « toujours n’effectuer la validation que dans un seul constructeur », de sorte qu’aucun objet invalide ne puisse exister
Pour modifier un objet, il faut l’implémenter de façon à reconstruire le nouvel état en rappelant ce même constructeur
Cela me rappelle la section 5 de qmail, qui contient « ne parsez pas » et « il y a les bonnes interfaces et les interfaces utilisateur »
Si j’enseignais un cours de programmation de niveau intermédiaire, je demanderais aux étudiants de rédiger un essai comparant et opposant ces propositions ; chacune apporte quelque chose à apprendre et elles peuvent sembler contradictoires au premier abord
Ressource liée : Richard Feldman, « Making Impossible States Impossible »
Discussions précédentes :
Transmis à Crowdstrike
Cela me rappelle un commentaire de quelqu’un à l’époque de la vague XML du milieu des années 2000 : beaucoup d’organisations ont choisi XML parce que XML leur fournissait un parseur
Même si écrire un parseur n’est ni difficile ni déplaisant, je ne comprends pas pourquoi les gens rechignent autant à en écrire
Je me demande si cela va à l’encontre de l’idée selon laquelle le mot-clé
requiredde Protocol Buffers était une grosse erreurLe mieux serait sans doute de disposer à la fois d’un parsing souple et non validé, et de fonctionnalités de parsing validé