3 points par GN⁺ 2024-07-23 | 1 commentaires | Partager sur WhatsApp

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

 
GN⁺ 2024-07-23
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é spec de 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

    • Exemple de code :
      if(!Whatever.TryParse<Thingy>(input, out var output)) output = some-sane-default;
      
    • Exemple de code :
      if(!Whatever.TryParse<Thingy>(input, out var output)) throw new ApplicationException($"Not a valid Thingy: {input}");
      
    • Il est recommandé de ne pas utiliser la seconde forme dans les pilotes en mode noyau
  • 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é required de Protocol Buffers était une grosse erreur

  • Le mieux serait sans doute de disposer à la fois d’un parsing souple et non validé, et de fonctionnalités de parsing validé