Les types statiques et la pelle
(carefully.understood.systems)- La baisse de popularité des types statiques entre les années 2000 et le début des années 2010, puis leur regain d’intérêt au milieu et à la fin des années 2010, s’expliquent par l’amélioration de la qualité des systèmes de types statiques
- Les systèmes de types dynamiques sont comparés au fait de creuser la terre à mains nues : il faut juger soi-même l’état et le contenu des variables et des champs, sans que l’ordinateur n’aide ni n’entrave ce travail
- Les anciens systèmes de types statiques, comme ceux du Java des débuts ou de C++98, sont comparés à une pelle en papier : ils n’aident même pas à distinguer les pointeurs nullable et non-nullable, et obligent à répéter les noms de types
- Les systèmes de types modernes comme TypeScript, Haskell, MyPy, Swift et Rust prennent mieux en charge la gestion de
null, les types somme et les types union, ainsi que l’inférence de types, ce qui améliore la détection d’erreurs et l’expression des états d’un programme - Avec la généralisation de fonctions d’IDE comme l’autocomplétion des noms de méthodes, les informations fournies au système de types statiques apportent non seulement des vérifications d’erreurs, mais aussi des gains de productivité
Argument principal
- L’idée est que le retour en grâce des types statiques ne relève pas d’un simple effet de mode, mais du fait que la qualité des systèmes de types statiques réellement utilisables s’est améliorée
- L’article utilise l’analogie suivante : avec une bonne pelle, mieux vaut creuser avec une pelle qu’à mains nues ; mais si l’on ne dispose que d’une pelle en papier, alors les mains nues valent mieux
- Dans un système de types dynamiques, c’est au développeur de réfléchir lui-même à l’état et au contenu des variables et des champs d’un programme, sans aide de l’ordinateur
- Un mauvais système de types statiques peut devenir plus contraignant qu’utile, ce qui est comparé au fait de creuser avec une pelle en papier
Limites des anciens systèmes de types statiques
- Les premiers systèmes de types statiques largement utilisés dans les années 1990 et au début des années 2000, comme ceux de Java ou de C++98, n’aidaient même pas correctement pour une tâche simple comme distinguer les pointeurs nullable et non-nullable
- Les anciens systèmes de types statiques sont décrits comme des structures sans types somme, avec uniquement des types produit
- Ils forçaient à écrire manuellement les noms de types à de nombreux endroits
- Un code comme
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));est qualifié de petite catastrophe
Améliorations des systèmes de types modernes
- Les systèmes de types modernes comme TypeScript, Haskell, MyPy, Swift et Rust proposent des moyens de distinguer les types nullable des types non-nullable
- Les exemples donnés sont
Maybe ten Haskell,T | nullen TypeScript,T?en Swift etOptional<T>en Rust ; le système de types peut alors signaler les endroits où une vérification denullest nécessaire et ceux où elle manque - En pratique, cela permettrait de ne presque plus voir d’erreurs de pointeur nul à l’exécution
- Les systèmes de types modernes offrent au moins l’un des deux mécanismes suivants, souvent les deux : types somme ou types union, ce qui permet de mettre en pratique "Make invalid states unrepresentable"
- Cette approche permet, dans des objets représentant des machines à états, de n’exprimer chaque champ que lorsqu’il existe réellement pour l’état concerné
- Les systèmes de types modernes fournissent aussi l’inférence de types : si le compilateur peut comprendre que
let x = 5;est un nombre, il n’est pas nécessaire d’écrirelet x: number = 5;
Fonctionnalités d’IDE et conclusion
- Avec la généralisation des fonctions d’IDE comme l’autocomplétion des noms de méthodes, l’utilité des systèmes de types statiques a encore augmenté
- Dans les années 1990, Intellisense était une fonctionnalité phare de Visual Studio ; dans les années 2020, des fonctions similaires existent dans presque tous les IDE et éditeurs
- Les informations fournies au système de types statiques servent non seulement à vérifier les erreurs dans le programme, mais apportent aussi des gains de productivité supplémentaires
- Un bon système de types dynamiques vaut mieux qu’un mauvais système de types statiques, mais aujourd’hui on peut utiliser des systèmes de types statiques bien meilleurs qu’autrefois
1 commentaires
Avis sur Lobste.rs
Cet article est bon, mais je ne suis pas totalement d’accord. Même si les systèmes de types statiques du début des années 2000 n’étaient pas excellents, ils étaient selon moi bien préférables à l’absence totale de typage statique
Il n’y avait pas de types somme fermés, mais on pouvait modéliser une grande partie avec le sous-typage, et il n’y avait pas de types non-null, mais les références et types non pointeurs de C++, ainsi que les types primitifs de Java, en couvraient une partie. En Ruby ou JavaScript, non seulement tous les types pouvaient être null, mais ils pouvaient aussi être traités comme des chaînes, comme des entiers, ou comme n’importe quel autre type du programme, ce qui était pire
Je pense qu’un grand facteur du changement de tendance autour du typage statique est que, pendant le boom des réseaux sociaux du Web 2.0, l’avantage du premier arrivé comptait plus que tout. Mieux valait lancer vite et itérer, quitte à accumuler de la dette technique en Ruby ou Python, que de se faire dépasser comme Friendster ou Digg, et si c’était lent, on pouvait simplement acheter plus de serveurs avec l’argent à bas taux facilement disponible à l’époque
Ensuite, avec le boom mobile, les logiciels se sont mis à tourner sur des appareils utilisateurs limités et hors de contrôle, et les applications à typage dynamique lentes étaient tout simplement lentes en pratique, tandis qu’en cas d’erreur de type on ne pouvait pas se rétablir proprement via un gestionnaire global de réponse comme sur un serveur. Dans ce contexte, la sécurité et les performances du typage statique sont devenues bien plus convaincantes
Au début des années 2000, j’étais d’accord aussi, car les systèmes de types de l’époque imposaient souvent des contraintes qui n’aidaient pas à structurer le code, tout en ne garantissant que des propriétés presque jamais erronées. En particulier, la combinaison du sous-typage et de l’héritage d’implémentation manquait de souplesse
J’ai changé d’avis en utilisant des systèmes de types plus modernes. Dans snmalloc, le système de types C++ impose une machine à états de propriété mémoire, et dans d’autres bases de code il vérifie le bon comportement d’overflow de compteurs de buffers circulaires. Dans les deux cas, quand on se trompe, le débogage est pénible et c’est une source d’erreurs fréquente, mais le compilateur a réellement refusé de compiler du code que je pensais correct, empêchant ainsi des bugs d’entrer dans la branche principale
Dans un IDE, appuyer sur
.puis taper quelques lettres du nom de la méthode avant de valider la bonne suggestion permet d’économiser 2 secondes toutes les quelques secondes, et fait aussi gagner les 30 secondes qu’il faudrait autrement pour aller chercher la définition d’une classe quand on ne sait pas quelles méthodes existent. Ce principe est aussi bien décrit sur https://grugbrain.dev/#grug-on-type-systemsOn écrit bien plus souvent des lignes qui appellent des méthodes que des annotations de type de paramètres de fonctions, donc le compromis est massivement défavorable au typage dynamique. Ce qui avait de la valeur, ce n’était pas d’autoriser du code absurde qui échoue à l’exécution, mais de pouvoir omettre le type des variables locales, et les langages à typage statique n’ont jamais eu besoin de l’interdire
Dans les rares bases de code qui prenaient le système de types au sérieux, on accumulait des pages de code qui ne disaient rien, tout en gardant des montagnes de conditions d’exécution, et dans le cas de Java, plus la hiérarchie de types grossissait, plus le programme devenait concrètement lent. La plupart des bases de code utilisaient les types de manière incomplète tout en ajoutant beaucoup de conditions à l’exécution, sans réduire énormément la couverture de tests nécessaire par rapport à un système dynamique
Les langages dynamiques n’apportaient aucun bénéfice statique, mais ils étaient concis, donc plus faciles à lire, à relire et à tester. C’était particulièrement vrai dans des environnements comme les frameworks d’injection de dépendances de la fin des années 90 et du début des années 2000, où l’ajout d’un nouveau service imposait de modifier plusieurs fichiers XML. On pouvait aussi travailler sans IDE consommant la moitié de la RAM
C’est exactement à ça qu’a ressemblé le début de ma carrière, donc je suis entièrement d’accord avec l’article. Le rapport coût/bénéfice de Java 1.4 à Java 6 était tellement mauvais que j’ai presque abandonné les langages à typage statique, et ce n’est que quelques années plus tard, en touchant à Haskell comme hobby, que j’ai compris que le typage statique pouvait lui aussi avoir un rapport coût/bénéfice raisonnable, et que le problème, c’était Java. L’essai “python is not java” illustre aussi très bien cette période sombre
Je doute qu’on ait réellement constaté dans nos logiciels des gains de fiabilité depuis que le typage statique est devenu l’air du temps
J’ai toujours pensé que les avantages du typage statique tenaient bien davantage au feedback immédiat pendant le développement et à la réduction des échecs critiques à l’exécution, mais même si de tels échecs restent théoriquement possibles, j’ai l’impression qu’en pratique ils ne surviennent pas si souvent
undefinedetnullont chuté brutalementDes juniors et même certains seniors étaient sceptiques au début, pensant qu’on allait se retrouver avec des
@ts-ignorepartout, mais en pratique il n’y en a eu qu’environ trois, y compris ceux dus à des types cassés dans des dépendances. Avant, il arrivait environ une fois par semaine qu’un conflit de types fasse planter l’app sur la branche de développement et bloque mon travail ; maintenant, je ne me souviens même plus de la dernière fois que c’est arrivéLe simple fait de satisfaire
tscréduit les bugs liés aux types, y compris quand le code n’a pas été écrit par moi. À l’inverse, les linters sont devenus trop zélés ces temps-ci, et à force de vouloir satisfaire des outils comme Sonar, j’ai vu de vraies régressions de refactoring. 95 % des alertes étaient fausses, 3 % venaient de bugs de l’outil, et les 2 % utiles ne pointaient même pas vers la vraie cause des bugs. Au lieu de passer une semaine à aligner la base de code pour corriger un bug, j’en ai ajouté deux autres au passageLe travail pour satisfaire
tscproduisait en gros deux corrections de bugs purs et une régression par jour, mais les régressions étaient en général moins graves, plutôt du mauvais comportement qu’un crash completEn y ajoutant des tests basés sur les propriétés, cela prenait en moyenne 2 à 4 heures et révélait toujours au moins un bug. Si votre code se prête aux tests basés sur les propriétés, il faut en faire
En augmentant la couverture de test avec le modèle bon marché DeepSeek V4 Flash tout en faisant attention à ne pas générer de tests poubelle, j’ai corrigé environ 2 à 3 bugs logiques par jour, sans crash. En revanche, le lot de tests reste tout juste maintenable
Quand on a laissé des juniors bricoler des tests avec des modèles de la famille Sonnet et Opus 4.5, 4.6, les modèles n’ont produit que des tests qui « documentaient le comportement actuel », donc l’effet des corrections a été faible, et le lot de tests était impossible à maintenir, au point qu’il a fallu le jeter
Les tests basés sur des modèles sont très bons pour attraper des bugs, mais la configuration est complexe, et il est très pénible de les pousser à explorer les recoins plutôt que de brûler des cycles sur des fonctionnalités de surface. Un genre de fuzzer basé sur des modèles et piloté par profil serait intéressant
En résumé, les vérificateurs de types attrapent bien les défaillances critiques et nombre de confusions, et les tests basés sur les propriétés sont excellents. Les tests classiques demandent beaucoup de discipline pour offrir un bénéfice régulier
C’est surtout l’assimilation de TypeScript à un bon système de types avec laquelle j’ai du mal à être d’accord
awaitm’a mordu plusieurs fois. Cela dit, il a bel et bien amélioré la situation de façon spectaculaireHonnêtement, j’ai fini par accepter le typage structurel aussi, et je pense que cela aura une influence positive sur la conception des langages à venir
Cet argument n’est pas très convaincant. Des langages de programmation corrects avec types de données algébriques et inférence de types existaient déjà depuis le milieu des années 1990
Les systèmes de types de Java et C++ étaient très pauvres, mais SML, OCaml et Haskell existaient déjà, avec un ressenti assez proche d’aujourd’hui. Si les gens n’utilisaient pas ces langages, c’est un problème de culture, d’adoption et d’exigences implicites, pas quelque chose qu’on peut expliquer uniquement par « les systèmes de types utilisables n’étaient pas assez bons »
Ou bien, si l’argument est « les systèmes de types des langages populaires de l’époque étaient mauvais, et ceux des langages populaires d’aujourd’hui sont meilleurs, donc les systèmes de types sont devenus plus populaires », cela ressemble à un raisonnement circulaire
Il y a aussi énormément de nuances dans la différence entre les langages conçus avec un système de types dès le départ et ceux conçus sans types à l’origine, puis dotés d’un système de types plus tard
Même en ayant toujours eu une préférence pour le typage dynamique, je trouve cet article assez juste. Aujourd’hui je travaille en C#, j’utilise Lisp pour mes loisirs, et j’ai aussi utilisé Python auparavant
Quand j’étais obligé d’utiliser Java 5, je me battais en permanence contre le système de types, généralement à cause de mauvaises décisions prises par les auteurs de bibliothèques. Après être passé à C# vers 2010, le système de types n’était plus activement nuisible, mais restait pour l’essentiel redondant, et il n’empêchait même pas les exceptions de pointeur nul, qui étaient la confusion de types la plus fréquente en Python
Le système de types de C# n’a vraiment commencé à m’aider qu’autour de 2020, avec l’arrivée des types de référence non nullables. Cette année, il accueille aussi des types union natifs, mais des bibliothèques de types union imposant l’exhaustivité étaient possibles au moins depuis 2016, et j’ai commencé à en utiliser à partir de 2020
Je pense que l’effet de mode joue encore un rôle, mais qu’une partie de cet effet n’est pas mauvaise. Des langages à la mode dotés de systèmes de types plus expressifs ont aussi apporté des améliorations aux langages ordinaires que nous utilisons au travail contre rémunération
Haskell et son système de types existaient déjà dans les années 2000. Ce n’était pas aussi largement utilisé qu’aujourd’hui, mais cela existait clairement, donc cette affirmation devrait être nuancée sur ce point
Personnellement, je pense que TypeScript a été un facteur majeur pour familiariser les utilisateurs de langages grand public avec de meilleurs systèmes de types. Au-delà de sa qualité et du soutien de Microsoft, il avait l’avantage de s’appliquer à JavaScript, et JavaScript avait plus urgemment besoin de types que Python. À cause de « Undefined is not a function. » et de « The good parts. »
« Real World Haskell » est sorti en 2008 et visait à rendre Haskell plus attractif pour les programmeurs grand public. Je ne sais pas dans quelle mesure cela a aidé à diffuser la bonne parole
Dans l’univers Java, Scala a apporté des types sophistiqués en 2004, et .NET a eu F# en 2005. Scala a peut-être obtenu les utilisateurs les plus visibles, comme Twitter, mais il n’était pas dans une position lui permettant d’absorber une grande part des utilisateurs de sa plateforme comme TypeScript, ni assez attractif pour attirer massivement des utilisateurs d’autres langages comme Rust ou Go
Dans le paragraphe suivant, il mentionne Haskell comme un « système de types moderne », mais à la fin des années 1990 et au début des années 2000, les personnes ayant une expérience de Haskell — même simplement pour y avoir touché un peu — représentaient en pratique presque 0 %. Le texte parle de la manière dont l’immense majorité des développeurs faisait alors l’expérience des langages à typage statique, et de la raison pour laquelle cette majorité les évitait collectivement
Par exemple, pour utiliser
duneen OCaml, il faut comprendre les fichiersopam, les fichiersdune, la syntaxe desocaml moduleet la syntaxe d’ocaml. Les extensions de compilateur optionnelles de Haskell me paraissent tout aussi intimidantesÀ comparer avec
cargo, où il suffit de connaître letomlet Rust