1 points par GN⁺ 2023-10-05 | 1 commentaires | Partager sur WhatsApp
  • 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 PersonId et PetId, 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 comportement
    • birthdayGreeting2(name, age) laisse deviner qu’il y a un nom et un âge, mais sans type
    • birthdayGreeting3(name: string, age: number): string inclut 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 age soit 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 Person montre bien la différence
    • birthdayGreeting3(person: Person) permet de retrouver facilement dans l’IDE les points d’usage de Person
    • avec la version non typée birthdayGreeting2(person), il faut parcourir toute la base de code pour comprendre qu’elle attend en réalité un Person
  • 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 de preson-{id}
    • une tentative de charger des données de personne comme un type Pet
  • 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érer cache.get(PersonCacheKey::new(id)) comme un Pet échouera à la compilation
  • Même de simples ID sous forme de String sont propices aux erreurs
    • do_something(id: String) ne dit pas clairement quel type d’ID est attendu
    • on peut passer pet.id alors qu’il fallait en réalité pet.owner
  • Svix définit donc un type distinct pour chaque ID
    • PersonId(String)
    • PetId(String)
    • owner dans Pet est de type PersonId
  • 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
    • PetId ne peut pas être créé sans validation
    • avec cette approche, lorsqu’on renvoie 404 Not Found parce 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 422 ou 400

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
  • 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 vim contre emacs ou 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

 
GN⁺ 2023-10-05
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

    • La plupart des gens semblent d’accord pour dire que les types empêchent de nombreux bugs, et ce fil contient aussi des études allant dans ce sens
      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
    • Au bout du compte, il faudra sans doute se contenter d’une question de jugement
      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
    • Je pense à peu près la même chose
      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
    • Le typage statique n’est qu’une tranche de gruyère de plus pour produire des logiciels plus fiables
      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
    • On dirait que certains pensent qu’écrire un programme dans un langage servant à exprimer des types fait disparaître comme par magie les « bugs runtime »
      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

    • Si le typage dynamique poussait réellement la plupart des développeurs à écrire du code simple, ce serait un argument très fort
      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
    • L’affirmation selon laquelle « le système de types empêche les développeurs débutants de comprendre et les pousse seulement à supprimer les erreurs rouges » me paraît, à première vue, exactement inverse
      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
    • J’ai du mal à comprendre, tant presque tout ce raisonnement me paraît inversé
      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
    • C’est une bonne remarque que les tours d’ivoire et la pression sociale peuvent donner envie d’éviter l’adoption de certaines technologies
      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 »
    • J’allais écrire presque exactement la même réponse
      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à

    • Je suis l’auteur. Il y a une chose, dans notre manière de faire chez Svix, que je n’ai évoquée que brièvement dans un paragraphe et que j’aurais dû expliquer davantage
      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ême si les noms et les adresses sont tous deux des chaînes quand on les regarde de près, ils doivent tout de même être utilisés comme des types distincts, et non comme de simples chaînes
      Mélanger un champ nom et un champ adresse est presque toujours une erreur, et le système de types peut l’empêcher
    • Le fait que la plupart des données transitent en JSON rend-il vraiment les choses si chaotiques ? Dans certains cas, une Map est aussi un type parfaitement raisonnable
  • 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

    • J’ai déjà vu des définitions de types TypeScript complètement folles
      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 »
    • Avant, je pensais qu’avec 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 compilateur
      Mais 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
    • Je me demande vraiment comment travaillent les gens qui modifient effectivement les signatures de fonctions de cette manière
      Ont-ils une méthodologie globale, comme des tests très stricts avec 100 % de couverture de code ?
    • Chaque fois que je vois ce genre de plainte sur le typage statique, je me demande comment les définitions de types et l’architecture ont été conçues pour que cela devienne un problème
      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
    • Je ne vois pas très bien comment ces deux points se rattachent au débat sur les types
      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

    • Tout à fait d’accord. L’argument de la maintenabilité est presque entièrement réglé par cela
      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
    • On pourrait aussi soutenir que c’est précisément cette réflexion prudente qui rend le logiciel plus propre et meilleur
      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

    • C’est forcément un débat frustrant, parce qu’au final chacun tire de son expérience des conclusions complètement différentes
      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
    • Voir les définitions et variables dans un REPL et un IDE, avec autocomplétion et refactoring, c’est bien, mais cela ne fonctionne que lorsque le code que l’on veut inspecter est effectivement en cours d’exécution. D’après mon expérience, cette approche ne passe pas bien à l’échelle
      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
    • Pour ma part, j’ai au contraire trouvé cela beaucoup plus facile avec le typage statique
      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
    • C’est peut-être votre expérience, mais ce n’est pas la mienne. Je ne pense pas qu’il existe une bonne réponse valable pour tout le monde. Il suffit de choisir ce qui vous convient et de passer à autre chose
  • 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.

    • C++ est, au-delà de son système de types, un langage globalement assez rude pour être productif.
      À 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++.
    • En Python, je code surtout dans un style fonctionnel, mais les types aident quand même beaucoup.
      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.
    • La vidéo de la WWDC d’il y a quelques années sur la programmation orientée protocoles (Swift) pourrait vous plaire : https://www.youtube.com/watch?v=p3zo4ptMBiQ
  • 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 birthdayGreeting de l’article. L’auteur se réjouit que, dans birthdayGreeting("John", "20"), le typage statique détecte un bug parce que "20" n’est pas un nombre. Mais il ne détecte pas birthdayGreeting(" ", 123). " " n’est pas un nom. Il ne détecte pas non plus birthdayGreeting("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é.

    • Une bonne partie de ces exemples pourrait tout à fait être détectée statiquement, selon le système de types. Mais le plus important, c’est l’affirmation selon laquelle « les bugs de type sont trivialement faciles à repérer ».
      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.
    • Je suis l’auteur de l’article. À mon avis, ces exemples ne sont pas des contre-exemples ; ils illustrent plutôt mon propos.
      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 Name est 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) et birthdayGreeting("Anna," -12335) sont en fait valides en JS, puisque number est 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.
    • Ce que je trouve intéressant dans ces exemples, c’est qu’à mes yeux il faudrait un type Name représentant toujours un nom valide, et un type Age repré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 birthdayGreeting peuvent 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.
    • La mention de l’explosion d’une fusée me fait immédiatement penser à [1]. Cet accident célèbre était lié à une vérification de types où les plages autorisées avaient été définies à l’avance pour certains types, ce qui rejoint aussi ton argument. Faire en sorte que birthdayGreeting accepte 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
    • Je ne vois pas en quoi le fait que le compilateur ne vérifie que la correction des types, et non celle du programme, justifie de dire que c’est une perte de temps.
      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 auto
    Cela 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 for en C++ est révolue
    Les 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

    • Le système de types de Python est plutôt correct compte tenu du contexte
      Il offre de bonnes fonctionnalités, comme Optional qui force à vérifier avant d’utiliser None, ou le sous-typage structurel via typing.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éussi
      Le 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/**kwargs parce qu’ils n’ont pas envie d’écrire de vraies signatures de méthodes
      Il 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 dynamic pour tous les types en C#, ou faire en sorte que toutes les méthodes Go acceptent interface{}. Mais Python a longtemps activement encouragé cette approche, et encore aujourd’hui beaucoup de tutoriels pour débutants présentent « accepter *kwargs permet de ne pas changer la signature d’une fonction » comme une fonctionnalité avancée pour gens malins, plutôt que comme un piège horrible
    • Les « systèmes de types » de Python et de TypeScript ont été conçus pour l’introduction progressive du typage dans des environnements non greenfield
      C’est indispensable pour les migrations progressives, et il est tout à fait compréhensible qu’ils fonctionnent ainsi
    • Flavors et CLOS ne sont pas des « systèmes de types ». Ce sont des « systèmes d’objets », et ils ne sont pas particulièrement laids
      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
    • Le système de types de TypeScript est vraiment étonnamment excellent. J’aimerais que davantage de systèmes de types soient aussi expressifs
  • 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

    • Je pense qu’il n’y a pas grand-chose à contester dans l’idée que « les types réduisent les bugs lorsqu’on modifie une base de code durable »
      É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
    • Comme c’est une affirmation très forte, prenons un cas extrême : les étapes intermédiaires formellement vérifiées de CompCert : https://users.cs.utah.edu/~regehr/papers/pldi11-preprint.pdf
      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
    • Je préfère largement le typage statique fort parce qu’il rend le code auto-documenté
      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
    • L’argument du typage statique est que, précisément parce que les types sont statiques, il rend impossibles les bugs de type
      Il n’y a pas d’attachement émotionnel, sinon la colère face au « mais ça n’élimine pas tous les bugs ! »
    • Que ressentiriez-vous si quelqu’un affirmait que « les arguments émotionnels peuvent être produits plus vite que les arguments rationnels » ?
      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