- Fort de plus de 20 ans d’expérience dans le développement logiciel, l’auteur estime que le typage statique fort vaut presque toujours le détour, sauf exceptions comme les REPL ou les scripts jetables
- Les types laissent dans le code un contrat entre l’appelant et l’appelé, ce qui permet d’écarter paramètres ou valeurs de retour invalides à la compilation ou lors de la vérification de types
- L’exemple d’une chaîne
"20"issue d’un champ HTML utilisée comme un nombre et produisant"201"montre la différence entre une erreur détectée avant l’exécution et une erreur visible par le client - Svix cherche à faire entrer dans le système de types les clés Redis, les valeurs de cache, les identifiants comme
PersonIdetPetId, ainsi que la validation des entrées API, afin de réduire les fautes de frappe et les mauvais passages d’ID - Omettre les types peut accélérer l’implémentation initiale, mais augmente les coûts de documentation, de tests et de débogage ; avec l’inférence de types et le support de l’IDE, le refactoring et l’onboarding deviennent plus simples
Pourquoi s’obstiner sur le typage statique
- Le typage statique fort n’est pas seulement une bonne idée : pour la plupart des logiciels, c’est presque le bon choix par défaut
- Les langages sans types, ou avec des variantes plus souples, ont aussi leur utilité
- usage en REPL
- scripts jetables dans des environnements déjà peu typés, par exemple le shell
- En dehors de cela, l’auteur préfère un typage fort dans la majorité des cas
- Ne pas utiliser de types peut sembler accélérer le développement à court terme, mais c’est selon lui proche de « foncer à pleine vitesse vers une falaise »
- Au fond, il n’y a que deux options
- travailler davantage pour vérifier les invariants à la compilation ou lors de la vérification de types
- travailler moins et vérifier à l’exécution, voire ne pas vérifier du tout à l’exécution
- Les erreurs d’exécution ne sont pas toujours détectées pendant le développement et, même lorsqu’elles le sont, elles peuvent apparaître d’une manière visible pour le client
- Les tests aident, mais il est difficile de couvrir tous les mauvais types possibles passés aux fonctions ; empêcher les mauvais types via le système de types est considéré comme plus simple
Les types sont directement liés aux contrats de code et à la réduction des bugs
- Les types sont à la fois des annotations de code utiles aux humains et aux outils, et un moyen de rendre plus stricts les contrats entre morceaux de code
- Même avec une simple fonction de message d’anniversaire, la clarté du contrat varie fortement
birthdayGreeting1(...params)ne révèle même pas le nombre de paramètres, ce qui oblige à lire la documentation pour comprendre le comportementbirthdayGreeting2(name, age)laisse deviner qu’il y a un nom et un âge, mais sans typebirthdayGreeting3(name: string, age: number): stringinclut dans le contrat les types d’entrée et de retour
- Si la fonction évolue pour utiliser
age + 1, la version non typée pose problème avec une entrée de type chaîne- une valeur provenant d’un champ HTML peut toujours être une chaîne
birthdayGreeting2("John", "20")renvoie"John will turn 201 next year!"- la version typée exige que
agesoit un nombre, donc l’appel invalide échoue à la compilation
- Le contrat entre l’appelant et l’appelé devient de plus en plus important à mesure que la base de code grandit
- il permet de savoir comment les appelants seront affectés quand l’appelé change
- c’est particulièrement important quand appelants et appelés sont écrits par des personnes différentes, comme dans une bibliothèque open source
- Sans ce type de contrat, il devient difficile de savoir jusqu’où les changements auront des effets
Avantages pour l’expérience de développement, le refactoring et l’onboarding
- Les informations de type permettent aux IDE et aux outils de développement d’améliorer fortement l’expérience de développement
- Quand une hypothèse est fausse, on le sait immédiatement en écrivant le code, ce qui réduit la charge cognitive
- Les développeurs n’ont pas besoin de mémoriser les types de toutes les variables et fonctions du contexte courant : le compilateur signale les incohérences
- Le refactoring devient aussi plus facile
- lorsqu’on change l’implémentation d’une fonction, le compilateur peut indiquer si des hypothèses faites ailleurs sont brisées
- Il est aussi plus simple pour un nouvel ingénieur de se familiariser avec une base de code ou une bibliothèque
- il peut suivre les définitions de types pour comprendre où elles sont utilisées
- il peut expérimenter facilement, puisque toute modification déclenchant une erreur de compilation sera immédiatement visible
- L’exemple d’une fonction qui prend un type
Personmontre bien la différencebirthdayGreeting3(person: Person)permet de retrouver facilement dans l’IDE les points d’usage dePerson- avec la version non typée
birthdayGreeting2(person), il faut parcourir toute la base de code pour comprendre qu’elle attend en réalité unPerson
- La documentation peut compenser en partie, mais elle vieillit facilement, alors que les types constituent une documentation directement intégrée au code
- Les types sont vus comme une forme plus forte de noms de variables bien choisis
Comment Svix injecte de l’information dans le système de types
- Svix cherche à faire entrer autant d’informations que possible dans le système de types afin de réduire les erreurs détectables à la compilation et d’améliorer l’expérience de développement
- Redis repose intrinsèquement sur un protocole orienté chaînes et n’a pas de types intégrés, ce qui peut faire perdre les bénéfices du typage à ce niveau
- Un exemple simple de cache contient deux bugs
- une faute de frappe dans le nom de clé, comme
person-{id}au lieu depreson-{id} - une tentative de charger des données de personne comme un type
Pet
- une faute de frappe dans le nom de clé, comme
- Pour éviter cela, Svix applique deux principes
- exiger des clés d’un type spécifique plutôt que de simples chaînes
- lier de force clés et valeurs
- Par exemple, si l’on utilise une clé créée avec
PersonCacheKey::new(id), alors du code qui tenterait de récupérercache.get(PersonCacheKey::new(id))comme unPetéchouera à la compilation - Même de simples ID sous forme de
Stringsont propices aux erreursdo_something(id: String)ne dit pas clairement quel type d’ID est attendu- on peut passer
pet.idalors qu’il fallait en réalitépet.owner
- Svix définit donc un type distinct pour chaque ID
PersonId(String)PetId(String)ownerdansPetest de typePersonId
- La validité des ID reçus via l’API est également liée à la création des types
- par exemple, un ID de pet a la forme d’un préfixe
pet_suivi d’un Ksuid PetIdne peut pas être créé sans validation- avec cette approche, lorsqu’on renvoie
404 Not Foundparce qu’un pet est introuvable en base, on peut être certain que le format de l’ID était lui-même valide - les ID invalides sont déjà traités dans le handler API avec une réponse
422ou400
- par exemple, un ID de pet a la forme d’un préfixe
Objections et rôle des outils
- Les principales objections au typage concernent la vitesse de développement, la courbe d’apprentissage et la complexité des types, ainsi que l’effort et le boilerplate
- Le prototypage sans types peut effectivement aller plus vite
- on peut commenter du code sans que le compilateur se plaigne
- on peut mettre des valeurs incorrectes dans des champs tant que les bonnes ne sont pas encore définies
- Mais cela constitue selon l’auteur une dette technique agressive et inutile, dont le coût sera payé plusieurs fois au débogage en local, dans la suite de tests et en production
- Il existe bien une courbe d’apprentissage, mais la plupart des gens n’ont pas besoin de devenir experts en types
- des expressions de types simples suffisent pour travailler efficacement
- lorsqu’on bloque, il suffit d’aller chercher de l’aide
- Les développeurs doivent déjà apprendre beaucoup de choses — le code, des frameworks comme React ou Axum, etc. — donc la charge liée à l’apprentissage des types est jugée exagérée
- Apprendre les types est un coût ponctuel, et les bénéfices qu’ils apportent pour l’onboarding dans une base de code donnée sont plus importants
- Sans types, il faut beaucoup de documentation et de tests pour retrouver une stabilité de base
- la documentation et les tests peuvent devenir obsolètes
- ajouter les bons types est considéré comme demandant moins d’effort
- Dans les langages sans inférence de types, écrire les types peut devenir fastidieux
- l’exemple Java montre des répétitions comme
Person person1 = newPerson(); - l’article ajoute ensuite une correction indiquant que Java dispose désormais d’inférence de types
- l’exemple Java montre des répétitions comme
- Dans un langage avec inférence comme Rust, on peut écrire plus simplement
let person1 = new_person(); - Pour profiter réellement des avantages du typage, il faut un éditeur de code ou un IDE avec autocomplétion moderne capable de comprendre le langage
- Contrairement aux débats de préférence comme
vimcontreemacsou tabulations contre espaces, l’auteur estime que le rapport coût/bénéfice du typage est tel qu’il lui est difficile de comprendre pourquoi on choisirait de s’en passer - Un article de suivi existe : using the type system effectively
1 commentaires
Avis sur Hacker News
Ce qu’il y a de plus frustrant dans cette discussion, c’est que tout le monde parle de ce que les gens ressentent, avec peu d’éléments empiriques
Les études existantes n’ont pas constaté de différence significative entre les deux approches, et en l’absence de nouvelles recherches, il est difficile d’affirmer avec certitude que la préférence de chacun est la bonne
Personnellement, j’aime les langages typés, mais des systèmes de types comme celui de TypeScript restent insuffisants. Comme on ne peut pas réellement utiliser les types à l’exécution, des bugs runtime subsistent, et comme beaucoup de logique runtime ne peut pas être encodée dans le système de types, on doit encore vérifier soi-même des cas impossibles
Si un système de types permettait de ne presque plus avoir à penser aux bugs runtime, ce serait un avantage écrasant, mais la plupart des langages n’en sont pas à ce niveau et restent dans un entre-deux ambigu entre surcharge et bénéfices partiels
Si l’on ne voit pas de grande différence en nombre de bugs ou en vitesse, c’est probablement parce que les effets finissent par se compenser. Sans filet de sécurité des types, on écrit davantage de tests ; à l’inverse, si l’on fait trop confiance au système de types, il reste au final une quantité comparable de bugs runtime. J’aimerais qu’il existe des études solides sur ce sujet, mais c’est un problème difficile
Le point central est plutôt de comprendre les raisons subjectives pour lesquelles on juge que l’investissement dans les types n’en vaut pas la peine
Il y a quelques années, j’ai examiné les recherches sur la productivité des développeurs, et elles étaient presque toutes médiocres ou ne s’appliquaient vraiment qu’aux juniors. Par exemple, les débutants tirent un grand bénéfice d’un retour rapide sur les erreurs statiques
Il est presque impossible d’appliquer un bon protocole expérimental à des professionnels plutôt qu’à des étudiants, et il faut isoler d’innombrables variables — différences individuelles, type de développement, mode de management, etc. — ce qui rend le signal difficile à extraire. C’est triste, mais beaucoup de choses dans la vie sont difficiles à mesurer efficacement
L’article et beaucoup de commentaires parlent de confort pour les programmeurs, de productivité et de « correction », mais les recherches actuelles ne montrent aucun résultat significatif indiquant que le typage statique améliore ou dégrade ces aspects. En pratique, c’est subjectif
En revanche, il y a un effet réel du typage statique que l’on peut prouver trivialement : il permet d’écrire du code plus efficace. C’est ce qui devrait être au centre des discussions sur la discipline de typage ; le reste, à ce stade, tient surtout de la spéculation
Le TypeScript mentionné dans l’article n’est pas réellement fortement typé : il est typé statiquement, mais faiblement typé. Les types y ressemblent davantage à des annotations, sans garantie de performance ni de disposition mémoire. Ainsi, en dehors de la documentation, on paie le coût du typage statique tout en obtenant très peu d’avantages concrets
Il est surprenant de voir la communauté tech ignorer les preuves réelles et accepter des préférences culturelles ou personnelles comme des faits
Comme les autres techniques, elle a des trous ; pour une fiabilité maximale, il faut donc combiner plusieurs techniques. Abandonner le typage statique parce qu’il ne détecte pas tout revient à refuser de verrouiller sa porte au motif qu’un voleur pourrait casser une fenêtre. Si la sécurité compte vraiment, il faut verrouiller la porte et mettre des barreaux aux fenêtres, pas choisir seulement l’un des deux
Si un langage est assez puissant pour écrire des programmes généraux, il est aussi assez puissant pour y introduire des bugs
Le typage statique peut être efficace pour détecter certains types de bugs, mais pas tous. Parfois, il améliore la lisibilité, comme des tests unitaires statiques ou un langage dédié servant de documentation exécutable
En général, les langages dynamiques sont plus agiles et permettent d’écrire davantage de tests plus facilement. Certains tests seraient inutiles dans un langage à typage statique, donc les types restent utiles, mais ils ne sont pas aussi universellement puissants qu’on le croit souvent
Indépendamment de la pression sociale qui voudrait qu’on aime le typage statique, la raison pour laquelle je m’en suis finalement éloigné est toujours la même : une tour d’ivoire s’est construite autour
J’ai développé des logiciels pendant dix ans dans chacun des deux paradigmes, et aujourd’hui je préfère ne pas utiliser de système de types
Le typage dynamique me donne l’impression d’une contrainte qui pousse à écrire du code simple, de la même manière que les tests unitaires imposent du code composable. Du code facile à lire et à comprendre
Je ne trouve pas non plus très convaincant l’argument selon lequel cela rendrait une codebase plus accessible aux développeurs débutants. Cela encourage facilement une boucle répétitive consistant à faire disparaître les soulignements rouges sans comprendre. Le système de types oblige, pour chaque projet, à apprendre par-dessus le langage un autre langage très spécifique au domaine, ce qui empêche souvent de comprendre le comportement réel
Les problèmes présentés dans l’article peuvent être résolus d’une manière aussi robuste que les types, mais plus facile à comprendre. On peut certes utiliser les types simplement, mais d’après mon expérience, ce n’est presque jamais ce qui arrive en pratique. Je n’aime pas non plus l’autocomplétion, prenez-le comme vous voulez
Je ne suis peut-être qu’un vieux développeur qui répète que « le code est la documentation », mais cette réflexion vient peut-être aussi d’une profonde frustration face à tous ces développeurs qui envahissent le secteur, pour qui « ChatGPT a dit que c’était bon » et qui touchent de gros salaires
Mais, en général, les preuves contraires semblent plus solides. Les types créés a posteriori pour documenter du code dynamique existant sont souvent beaucoup plus complexes que la même fonctionnalité implémentée dès le départ avec du typage statique. L’écosystème TypeScript, avec DefinitelyTyped, en fournit d’innombrables exemples
Il est difficile de dire que ces types sont « utilisés simplement », mais cette complexité ne vient pas du système de types lui-même ni de la manière dont les définitions de types sont fournies ; elle vient de la complexité du code dynamique qu’ils décrivent
Un package équivalent conçu dès le départ avec du typage statique a généralement une interface plus simple. Parce que les types sont définis en amont, au lieu d’être adaptés après coup à une API existante
J’irais même jusqu’à dire que, tant qu’une interface n’est pas explicitée, on ne peut pas savoir si elle est simple ou complexe. Je suis d’accord avec l’idéal selon lequel « le code est la documentation », mais s’il n’existe pas de code qui explicite l’interface, alors cette interface est, par définition, insuffisamment documentée
J’aimerais presque voir une telle scène de mes propres yeux. Dans mon domaine, la logique métier spécifique est presque incompréhensible dans une codebase à typage dynamique, alors que le code à typage statique apprend la logique métier aux développeurs
L’expression « le code est la documentation » me semble même plutôt confuse. D’après mon expérience, il faut du typage statique pour que le code devienne de la documentation. Sans cela, il n’y a aucun moyen de savoir quelles propriétés possède un objet, ni pourquoi on vérifie une propriété dont on pensait qu’elle n’existait pas. Il y a bien des commentaires, mais je vois très rarement des gens écrire des commentaires utiles
Mon expérience est contraire. Les motifs très dynamiques sont difficiles à typer correctement, et un bon système de types encourage des motifs plus simples, ce qui rend aussi les types plus simples
Faire disparaître les erreurs rouges est important. Un soulignement rouge signifie qu’il y a un problème, et c’est beaucoup plus facile que de découvrir l’erreur autrement. Je ne vois pas pourquoi on voudrait découvrir cette erreur plus tard
Le fait de dire que l’on n’aime pas non plus l’autocomplétion me fait pencher du côté de ceux qui se méfient des opposants au typage statique. Un programmeur qui ne veut pas que l’ordinateur l’aide à programmer me paraît très suspect
Mais cela n’enlève rien à leurs mérites techniques. Une technologie peut être excellente tout en étant entourée de gens très prétentieux
L’idée que le typage dynamique pousse à écrire du code simple ressemble à dire : « conduire avec des œillères, c’est bien parce que cela force à conduire lentement ». Si c’est l’objectif, autant utiliser un linter qui impose une longueur de ligne ou un nombre maximal de paramètres ; il n’est pas nécessaire de créer cette contrainte de manière indirecte
Les types ne sont pas la seule solution, mais leur retour sur investissement est très élevé, donc c’est l’outil que je sortirais en premier. L’investissement est presque nul et les bénéfices sont importants
Je suis d’accord avec l’idée que « le code est la documentation », mais les types font aussi partie du code. J’aimerais donc formuler cela ainsi : « le code est la documentation, et les types font partie du code »
Notre domaine relève de l’ingénierie, il n’existe pas une unique bonne réponse, et tout est affaire de compromis. C’est d’ailleurs précisément pour cela que notre travail ne sera pas immédiatement automatisé et supprimé
L’ambiance de ce fil, où l’on « regarde de haut » quelqu’un à cause de l’opinion ou de l’expérience d’un autre ingénieur, est vraiment désagréable
Dans un contexte où la plupart des données transitent sur le réseau en JSON, la bataille pour appliquer un typage statique fort est généralement menée de façon incohérente
Il faut utiliser tous les outils disponibles, mais la plupart des « données » sont bien plus molles qu’on ne le croit. Si les gens stockent les numéros de téléphone sous forme de chaînes, ce n’est pas par paresse, mais parce qu’ils se sont un jour imaginé pouvoir en faire un type plus fort et s’y sont trop souvent brûlé les doigts. Il en va de même pour les noms, les adresses et les codes postaux
Ces valeurs doivent être recueillies auprès des utilisateurs et, en pratique, il n’y a guère d’autre solution que de parser du texte. Si vous concevez un système qui ne conserve pas le texte d’origine avant parsing, vous le regretterez presque certainement un jour
La meilleure approche me semble être une couche qui conserve le texte saisi d’origine tout en fournissant aux utilisateurs du backend un ensemble de données typées, mais il faut se demander si, dans le petit périmètre de chacun, l’investissement en vaut vraiment la peine
Si l’évaluation est lourde, il faudra probablement une couche qui transforme cela en SAT ou en un autre modèle numérique. Dans ce monde-là, les nombres sont l’abstraction. Si l’on essaie de faire autrement, on souffrira presque à coup sûr. Il vaut mieux disposer d’une couche qui traduit le problème en formalisation et l’espace des solutions en domaine ; les types peuvent aider ici, mais les « types » qui retiennent réellement l’attention ne sont trop souvent pas ceux-là
Grâce à des bibliothèques comme Serde et Pydantic, nous suivons l’approche selon laquelle la désérialisation est la validation. Toutes les données JSON sont validées avant d’être transformées en structures de code
C’est similaire à l’exemple avec Redis : même si l’on reçoit du JSON par le réseau, une fois qu’il a été entièrement validé et qu’il arrive dans le code, on peut être sûr qu’il s’agit de types bien formés. Dans le code, on peut donc supposer qu’un type email est bien un email valide, et qu’un type ID est bien un ID valide
Mélanger un champ nom et un champ adresse est presque toujours une erreur, et le système de types peut l’empêcher
Si une faute de frappe devient une erreur d’exécution, ce n’est pas « aller plus vite » ; et si, quand on change la signature d’une fonction, il faut faire un grep dans la base de code pour trouver tous les points d’appel en espérant avoir tout corrigé, ce n’est pas non plus « plus productif »
Les types, c’est bien, mais tout excès pose problème. Si l’objectif de votre vie devient d’encoder toute la logique métier dans le système de types, on obtient un chaos encore plus incompréhensible que l’absence totale de types. Si le nom d’un type ne tient pas sur une ligne dans un message d’erreur, c’est que vous êtes allé trop loin
Pour être juste, c’était pour s’adapter à du vieux code JS pur, et cette pauvre variable pouvait contenir toutes sortes de valeurs
Je serai éternellement reconnaissant à TypeScript, mais je ne serais pas surpris de voir un jour ce genre de code cité dans un article intitulé « l’époque où les types étaient allés trop loin »
from pdb import set_trace: set_trace(), on pouvait faire de l’édition interactive, et qu’il valait donc mieux interagir avec le programme en cours d’exécution qu’avec un compilateurMais dès que le programme devient un peu complexe, la situation change. Quand on fait passer des données entre systèmes via des files, qu’on utilise de l’asynchrone, des threads, du multiprocessing, et des bibliothèques binaires compilées pour les parties critiques en performance, on finit par se dire qu’on aurait préféré tout écrire en Erlang
Ont-ils une méthodologie globale, comme des tests très stricts avec 100 % de couverture de code ?
Si les points d’appel ne sont pas détectés au moment du build ou de la compilation, ce n’est pas du typage statique que vous utilisez
Une faute de frappe peut produire du code incorrect même dans les langages statiquement typés les plus stricts. Sinon, qu’est-ce que cela voudrait dire, écrire du code ? Qu’est-ce qui est pire : une erreur d’exécution, ou un résultat faux sans aucune erreur ?
Lancer un projet Python peut aussi être plus rapide que compiler du C++, et les langages à typage dynamique peuvent offrir de meilleurs moyens que grep pour trouver les appels de fonction
Dire que l’absence de types a l’avantage d’accélérer le développement ne correspond pas non plus à mon expérience. Le typage statique rend la programmation quotidienne plus rapide
Le fait que les IDE s’améliorent grâce au typage statique a été abordé plus loin, mais je le ressens aussi dans le REPL. Les erreurs de type détectées statiquement donnent des messages d’erreur pertinents, beaucoup plus proches de la cause racine réelle, et permettent de corriger plus vite que les erreurs d’exécution
Cela réduit aussi la charge de devoir réfléchir avec trop de prudence aux types. Comme le compilateur fait respecter la discipline, j’ai moins besoin de m’en préoccuper. Je peux avancer plus vite avec la certitude qu’une grande catégorie d’erreurs sera détectée immédiatement
D’après mon expérience, les systèmes de types statiques sont faciles à utiliser, accélèrent le développement et améliorent la fiabilité. Jusqu’ici, je n’ai vu que deux coûts : ils peuvent être plus difficiles à apprendre, et plus difficiles à implémenter
Le développeur junior que l’on embauchera dans six mois mettra beaucoup plus de temps à s’adapter à du code non typé
J’admets que l’écriture initiale puisse être « plus rapide » pour certaines personnes, mais ensuite, tous les développeurs qui liront ce code seront ralentis
Il peut être préférable de réfléchir à ce qu’on met en entrée et en sortie, et pourquoi, plutôt que de produire un fouillis qui se contente de passer le compilateur
Je pense que l’auteur se trompe sur presque tous les points. J’ai pensé comme lui pendant des décennies, mais j’ai complètement changé d’avis ces dernières années
Les types réduisent les bugs ? Non. Peut-être très légèrement, mais pas de manière significative. Il suffit de regarder les études sur le sujet
Les types offrent une meilleure expérience de développement ? Non. Mon REPL et mon IDE contiennent toutes les définitions et toutes les variables. Je peux y faire l’autocomplétion de tous les symboles, explorer l’arbre d’appels, rechercher les usages, refactorer en confiance, exécuter, remplacer ou encapsuler une fonction seule, dans le REPL comme dans l’application
Tout encoder dans le système de types ? Impossible. Il faut de la validation à l’exécution
Bon courage aussi pour défaire les définitions de types quand les exigences changent. C’est là que tout se joue. Le typage statique fige trop tôt le modèle de données du domaine tel qu’on le comprend à un instant donné. Ce modèle change, et avec un peu de malchance il faut prendre en charge plusieurs variantes du modèle de domaine dans le même runtime. C’est encore pire si l’on a utilisé l’héritage
Il est vrai que le typage statique donne un gros levier aux optimisations du compilateur, mais certains langages à typage dynamique proposent aussi le typage statique comme fonctionnalité optionnelle
Pour de nombreux cas d’usage, notamment en développement d’entreprise, un langage fonctionnel à typage dynamique privilégiant l’immutabilité apporte de grands bénéfices à long terme
L’erreur de catégorie que commettent souvent les fervents partisans des types consiste à supposer que l’on écrirait le même code, simplement sans types. En réalité, on ne l’écrit pas ainsi
Sur presque tous les points, mes conclusions sont exactement opposées. Bien sûr qu’il faut de la validation à l’exécution, mais la plupart de cette validation peut être évitée
En ce qui concerne l’évolution des exigences, je trouve au contraire que le typage statique facilite l’adaptation. Dans les systèmes à typage dynamique que j’ai connus, les hypothèses importantes sur les structures de données étaient disséminées un peu partout, parfois vérifiées dynamiquement sous forme de préconditions et postconditions, parfois seulement présentes dans les tests, parfois pas vérifiées du tout
Pour changer les exigences, il fallait raisonner sur tous les effets de ces hypothèses implicites, ce qui rendait les changements effrayants. Il était facile de lancer l’application avec le nouveau code, mais très difficile de savoir si l’on avait cassé un chemin de code rare auquel on n’avait pas pensé
Je préfère de loin une étape d’analyse statique qui me dit : « Vous avez changé cette interface ; savez-vous que ce chemin de code dépendait de cette partie ? » Le typage statique n’est pas la seule manière d’y parvenir, mais je pense qu’il demande beaucoup moins d’efforts qu’un niveau équivalent de validation dynamique et de tests
Un système de types n’élimine pas la validation à l’exécution, mais s’il est bien utilisé, il la réduit spectaculairement
Quand les exigences changent, le meilleur aspect est que le compilateur indique précisément ce qu’il faut corriger pour que tout fonctionne de nouveau. Faire la même chose dans un langage dynamique oblige à tout retracer soi-même, à attendre qu’un test unitaire échoue, et à prier pour qu’aucun chemin n’ait échappé aux mailles du filet
Je pouvais trouver avec bien plus de confiance tous les endroits où un type donné était utilisé, puis vérifier pour chacun s’il nécessitait un changement. Dans un environnement à typage dynamique, cette tâche était beaucoup plus fragmentée et fastidieuse
Même en tant que développeur ayant écrit des centaines de milliers de lignes en C++, Python et JS, je ne sais pas trop. Ce n’est pas si évident.
Je suis productif dans les trois, mais Python l’emporte généralement. Cela dit, je n’écrirais pas un moteur de jeu ou un codec vidéo en Python.
JavaScript est incohérent et bizarre, mais l’héritage de Netscape nous y a déjà tous enchaînés.
Dans un style très orienté objet, avec beaucoup d’énormes classes imbriquées, le typage statique au moment de la compilation et de l’analyse peut réduire beaucoup d’erreurs. Mais j’en suis venu à considérer l’orienté objet comme globalement proche d’une catastrophe, et des fonctions simples avec des données structurées l’emportent presque toujours en simplicité et en maintenabilité.
Les serveurs de langage et IDE modernes peuvent aussi détecter beaucoup de fautes de frappe pendant le développement en JS/Python. Je suis pointilleux sur beaucoup d’aspects de la programmation, mais je n’ai jamais eu de position tranchée sur typage statique contre typage dynamique. Les deux comptent des millions de projets réussis.
À mon avis, le plus gros gain de productivité qu’un langage puisse offrir, c’est le ramasse-miettes. Je serais plus curieux de voir comment se compare Go, avec une syntaxe et des types bien plus simples. Java pourrait aussi faire mieux. Même s’il est verbeux, pour la plupart des tâches, sa charge cognitive n’est qu’une fraction de celle du C++.
Je fais aussi beaucoup de fautes de frappe et je me trompe souvent dans l’ordre des arguments. Les types aident particulièrement en machine learning. Voir le code d’entraînement planter après avoir passé 30 minutes à traiter les données, c’est bien la dernière chose dont on a envie.
Je trouve que le typage progressif de Python est un très bon compromis entre le prototypage rapide et l’ajout d’annotations de type une fois qu’une fonction est suffisamment mature.
Le débat sur le fait que le typage fort soit préférable au typage faible est réglé, mais celui de savoir si le typage statique est préférable au typage dynamique ne l’est pas.
Les partisans du typage statique pensent que le compilateur doit vérifier les invariants de type pour confirmer la « correction », tandis que les partisans du typage dynamique estiment que c’est une perte de temps.
Je suis clairement dans le second camp. Car le compilateur ne peut vérifier que la correction des types, pas celle du programme. La correction des types est nécessaire à la correction du programme, mais elle n’est pas suffisante. Les partisans du typage statique n’arrivent pas à l’admettre et s’imaginent que le typage statique garantit plus de choses qu’il ne le fait réellement.
Prenons l’exemple
birthdayGreetingde l’article. L’auteur se réjouit que, dansbirthdayGreeting("John", "20"), le typage statique détecte un bug parce que"20"n’est pas un nombre. Mais il ne détecte pasbirthdayGreeting(" ", 123)." "n’est pas un nom. Il ne détecte pas non plusbirthdayGreeting("Anna," -12335). À l’inverse,birthdayGreeting("Anna" 4.5)est détecté, alors que 4,5 peut être considéré comme un âge ; on peut donc dire que c’est plutôt une erreur.C’est important. Les « bugs de type » sont trivialement faciles à repérer, mais les bugs sémantiques peuvent rester cachés pendant des années : un dépassement de capacité sur le solde d’un compte stocké en
uint, un nombre qui devrait être premier à un endroit précis mais ne l’est pas, une liste qui ne devrait pas être vide, etc. Même les types dépendants ne peuvent pas garantir ce genre d’invariants.Si vous avez du mal à le croire, cherchez les gros bugs qui ont provoqué l’explosion de fusées ou des accidents automobiles. À ma connaissance, aucun n’était dû à une véritable erreur de type ; l’écrasante majorité relevait d’erreurs sémantiques.
[1] La plupart des gens ne comprennent pas qu’il faut considérer les types selon au moins deux axes, fort/faible et statique/dynamique, et continuent de confondre typage faible et typage dynamique. C est statiquement typé mais faiblement typé, Python est fortement typé et dynamiquement typé, et JavaScript est faiblement typé et dynamiquement typé.
C’est précisément pour cela que je suis du côté du typage statique. C’est tellement trivial qu’on peut le traiter de façon déclarative, juste à côté du code concerné, avec un retour immédiat, à chaque point d’appel et pour chaque sous-expression ou instruction.
Les annotations de type ne signifient pas que la sémantique ou la logique métier est correcte ; cela doit toujours être testé. Mais elles peuvent remplacer des dizaines de petits tests triviaux, orthogonaux à la logique qui nous intéresse. Honnêtement, presque personne n’écrit systématiquement tous ces tests.
Plus loin dans l’article, j’ai dit qu’on valide les types au moment de les créer, par exemple à partir d’entrées utilisateur. Donc le type
Nameest toujours valide, et" "n’est pas un nom. Comme le type garantit un nom valide, cela serait bien détecté dans notre base de code.birthdayGreeting("Anna" 4.5)etbirthdayGreeting("Anna," -12335)sont en fait valides en JS, puisquenumberest un nombre à virgule flottante. Cela dit, en écrivant l’article, j’avais des entiers en tête. C’est un autre exemple où un système de types plus strict que TS, par exemple Rust, aide à mieux définir les invariants.En résumé, en voulant donner un exemple simple, je n’ai pas défini tous les types aussi strictement que d’habitude, et cela a fait apparaître encore plus de bugs qui auraient été capturés par les types.
Namereprésentant toujours un nom valide, et un typeAgereprésentant toujours un âge valide.On place la validation dans un seul endroit, le constructeur de ces types, et les différentes méthodes comme
birthdayGreetingpeuvent ensuite utiliser des valeurs de ces types sans en porter la responsabilité.Je ne vois pas comment bien mettre en œuvre ce pattern sans vérification de types, ou au minimum sans annotations de type optionnelles et analyse statique. À la place, valider les entrées dans chaque méthode est beaucoup trop lourd, et supposer que l’appelant fournit des valeurs valides puis écrire des tests pour éviter une grosse catastrophe n’est pas satisfaisant non plus.
birthdayGreetingaccepte une plage de 1 à 150 est facile en Ada.Parmi les problèmes publics liés au spatial, il y en a quelques-uns qui auraient sans doute pu être détectés avec une meilleure vérification de types. Pour les conversions entre système métrique et unités impériales, il suffit d’intégrer les unités dans les types. Cela dit, dans le cas de [2], il est très probable que le problème relevait plutôt des tests d’intégration.
Bien sûr, la vérification de types ne peut pas trouver tous les problèmes de code, en particulier les problèmes algorithmiques, et ne remplace pas les tests. Mais le retour immédiat et les indications de type pendant le développement sont extrêmement précieux.
Dans l’exemple, on pourrait aussi remplacer la chaîne de caractères du nom par un type ou un objet personne.
[1]: https://en.m.wikipedia.org/wiki/Ariane_flight_V88
[2]: https://en.m.wikipedia.org/wiki/Mars_Climate_Orbiter
Il n’existe pas de solution parfaite, mais il existe beaucoup de solutions utiles.
Il y a eu une convergence considérable sur cette question. Aujourd’hui, la plupart des langages offrent un certain degré d’inférence de types au niveau des instructions. C++ a aussi
autoCela a fortement réduit le boilerplate lié aux types dans le code. L’époque où il fallait écrire en entier de longs types d’itérateurs dans les boucles
foren C++ est révolueLes déclarations de fonctions et les champs de structures sont les endroits où l’information de type est nécessaire pour lire le code. Dès qu’un programme dépasse quelques centaines de lignes ou qu’il y a plus d’un développeur, un certain niveau d’annotation est indispensable
L’opposition principale vient évidemment des utilisateurs de Python et de JavaScript. Python a greffé plus tard un système de types indicatif assez étrange, et JavaScript a greffé TypeScript plus tard. Dans les deux cas, ce sont des systèmes de types ajoutés après coup, utilisés dans des environnements où coexistent du code typé et non typé. C’est pénible
LISP aussi avait ajouté après coup, il y a des décennies, un système de types avec « flavors » et le Common LISP Object System, et ce n’était pas très élégant non plus. La leçon, c’est qu’ajouter un système de types après coup produit du désordre
Il offre de bonnes fonctionnalités, comme
Optionalqui force à vérifier avant d’utiliserNone, ou le sous-typage structurel viatyping.Protocol. Si Python avait été conçu dès le départ avec le typage en tête, cela aurait été mieux, mais compte tenu de la nécessité de s’intégrer au code Python existant sans rien casser, c’est plutôt réussiLe plus gros problème du typage statique en Python, ce sont l’écosystème et les conventions. C’est encore plus marqué parce que beaucoup de développeurs utilisent en réalité Python comme des data scientists. Ils abusent de
*args/**kwargsparce qu’ils n’ont pas envie d’écrire de vraies signatures de méthodesIl est très courant que des méthodes se passent des DataFrame ou des dictionnaires comme des sacs fourre-tout. Bonus si la méthode ajoute ou supprime des colonnes ou des champs, au point qu’on ne sait pas ce que contient le sac de données avant d’exécuter le code ou de lire toutes les lignes
Bien sûr, on peut faire quelque chose de similaire dans presque tous les langages. On peut utiliser
dynamicpour tous les types en C#, ou faire en sorte que toutes les méthodes Go acceptentinterface{}. Mais Python a longtemps activement encouragé cette approche, et encore aujourd’hui beaucoup de tutoriels pour débutants présentent « accepter*kwargspermet de ne pas changer la signature d’une fonction » comme une fonctionnalité avancée pour gens malins, plutôt que comme un piège horribleC’est indispensable pour les migrations progressives, et il est tout à fait compréhensible qu’ils fonctionnent ainsi
Flavors a été introduit dans un Lisp qui n’avait pas de système de types, puis CLOS a ensuite été ajouté à Common Lisp, un Lisp qui disposait déjà d’un système de types
Les gens ont toujours été fanatiques de ce à quoi ils s’accrochent émotionnellement plutôt que rationnellement
L’affirmation selon laquelle « les types réduisent les bugs » relève davantage de quelque chose qui semble plausible que d’un fait établi
https://blog.metaobject.com/2014/06/the-safyness-of-static-t...
Ce n’est pas faute d’avoir essayé. On pourrait dire que cette affirmation a été réfutée
Cela dit, personnellement, j’aime le typage statique. Principalement pour son effet de documentation, et ce n’est peut-être pas un hasard si c’est aussi le seul effet positif pour lequel il existe des preuves empiriques solides
Éviter les régressions peut être plus important qu’écrire du code correct dès le départ, et lorsqu’on examine du code qui n’évolue pas, on ne peut pas évaluer cet aspect
Concrètement, supprimer un champ d’objet dans un grand projet en JavaScript pur est intrinsèquement un champ de mines et a causé beaucoup de bugs par le passé. À l’inverse, dans un projet entièrement en TypeScript, on peut effectuer le même changement avec confiance
Je pense que cela peut être vu comme une preuve empirique que des types statiques très forts réduisent les bugs
En fin de compte, comme le disent beaucoup de commentaires, les types ne sont pas une question binaire oui/non, mais un vaste spectre sur plusieurs axes : statique/dynamique, fort/faible, etc. Les systèmes de types diffèrent beaucoup entre eux, tout comme la manière dont les gens les appliquent à un problème
Même dans un langage à typage statique fort, on peut représenter tout sous forme de chaînes et convertir en permanence ; c’est en pratique travailler comme dans un langage à typage dynamique. À l’inverse, si l’on utilise les outils fournis par le système de types pour créer des classes représentant des valeurs valides et affirmer des invariants importants, on en tire des bénéfices
La productivité augmente non pas de quelques fois, mais de plusieurs ordres de grandeur. Je dis cela après avoir beaucoup utilisé plusieurs langages couvrant une large partie de ce spectre, comme C, C++, Java, Python, JavaScript, TCL, etc.
Il devient beaucoup plus facile de raisonner sur du code qu’on n’a pas touché récemment, qu’il s’agisse du projet actuel ou d’une dépendance. On n’a pas besoin de partir sans cesse dans des chemins de traverse pour comprendre ce qu’on peut faire exactement avec l’objet retourné par une fonction, ce qui permet de rester davantage concentré sur le problème à résoudre
Il y a aussi le soulagement agréable quand la compilation passe, mais c’est secondaire
Il n’y a pas d’attachement émotionnel, sinon la colère face au « mais ça n’élimine pas tous les bugs ! »
Ce débat donne exactement cette impression
Je veux simplement la rationalité élémentaire d’un compilateur qui me dit « non » quand j’essaie de traiter une table de hachage comme un Apple ou une String