2 points par GN⁺ 2025-03-12 | 1 commentaires | Partager sur WhatsApp
  • Même lorsqu’un code est excellent sur le plan logique, il peut devenir pénible à lire dans la durée ; cet article attribue cette fatigue à la complexité visible à l’œil nu
  • Les Halstead Complexity Metrics comptent le nombre d’opérateurs et d’opérandes pour calculer le Volume et la Difficulty, et considèrent qu’une implémentation comportant davantage de variables et d’opérateurs est plus difficile, même à comportement identique
  • La Cognitive Complexity de SonarSource évalue la charge liée aux instructions abrégées, aux ruptures de flux linéaire et aux flux de contrôle imbriqués, notamment les conditions, boucles, exceptions, combinaisons d’opérateurs logiques, la récursion et goto
  • Du côté des variables, le masquage de variables (variable shadowing), les noms similaires, les longues durées de vie des variables et les usages inhabituels augmentent le coût de suivi du flux de données pour le lecteur
  • Petites fonctions, motifs familiers, regroupement des longues chaînes, conditions simples, usage limité de goto, imbrication faible, noms de variables distincts et durées de vie courtes des variables constituent des critères de lisibilité applicables au-delà des langages et du formatage

Limites et objectifs des métriques de lisibilité du code

  • Il n’existe pas de métrique unique, largement utilisée et consensuelle pour la lisibilité du code
  • Les références disponibles sont surtout des articles académiques peu utilisés en pratique ou des opinions ; il fallait donc des outils de discussion plus concrets, directement utilisables en revue de code
  • L’objectif n’est pas d’inventer une nouvelle métrique, mais de rassembler des motifs visuels que chacun peut utiliser lorsqu’il s’agit de discuter de la facilité de lecture d’un code
  • Les mesures ou idées examinées devaient répondre aux critères suivants
    • Être applicables à un fragment de code source ou à une fonction unique
    • Ne pas se concentrer uniquement sur la complexité essentielle, difficile à séparer de l’algorithme implémenté lui-même, comme la Cyclomatic Complexity
    • Ne pas se limiter à des éléments de style superficiels comme la longueur des noms de variables, les espaces, l’indentation ou le placement des parenthèses

Halstead Complexity Metrics

  • Maurice Halstead a proposé à la fin des années 1970 les Halstead Complexity Metrics afin de créer des mesures empiriques du code source
  • Ces métriques peuvent s’appliquer au-delà des langages et des plateformes, et se concentrent davantage sur la forme dans laquelle le code est écrit que sur l’algorithme implémenté lui-même
  • Les mesures de base sont quatre décomptes fondés sur les opérateurs et les opérandes
    • nombre d’opérateurs distincts n1
    • nombre d’opérandes distincts n2
    • nombre total d’opérateurs N1
    • nombre total d’opérandes N2
  • À partir de cela, Halstead a créé des métriques associées comme la length, le volume et la difficulty, et a même tenté d’en déduire une estimation du nombre de bugs dans une implémentation
  • Intuitivement, plus il y a d’opérateurs, plus il faut prendre en compte d’interactions potentielles ; plus il y a d’opérandes, plus il devient difficile de comprendre les flux de données possibles
  • Exemple JavaScript

    • Même pour une fonction de test pair/impair, une implémentation simple utilisant if et return contient peu d’opérateurs et d’opérandes
    • 4 opérateurs distincts, 7 opérateurs au total
    • 5 opérandes distincts, 6 opérandes au total
    • Volume 33,30, Difficulty 2,50
    • Une implémentation utilisant un tableau, Number, une expression de comparaison et un index comporte davantage d’opérateurs et d’opérandes
    • 7 opérateurs distincts, 10 opérateurs au total
    • 9 opérandes distincts, 12 opérandes au total
    • Volume 71,35, Difficulty 3,75
    • La première implémentation paraît aussi plus simple à l’œil, et les valeurs de Volume et de Difficulty de Halstead le confirment
    • L’inconvénient est qu’il n’est pas toujours évident, dans tous les langages, de déterminer ce qui doit être considéré comme operator ou operand ; pour mesurer, il vaut mieux choisir un outil ou une implémentation précise et l’utiliser de manière cohérente
  • Motifs pratiques tirés de Halstead

    • Plus une fonction est petite et contient peu de variables, plus elle est généralement facile à lire
    • Les opérateurs propres à un langage et le sucre syntaxique ajoutent une charge supplémentaire pour le lecteur ; mieux vaut donc éviter d’en abuser
    • Enchaîner longuement des composants fonctionnels comme map, reduce, filter, des lambdas, des itérateurs ou des compréhensions peut réduire la lisibilité, même si le code est concis
    • Ces longues chaînes peuvent se rencontrer plus souvent en JavaScript et en Rust, ou dans du code Python très poussé avec itertools

La difficulté de lecture selon la Cognitive Complexity

  • La Cognitive Complexity, créée par SonarSource, est une métrique destinée à mieux saisir ce qui rend le code difficile à lire
  • L’idée centrale de cette métrique repose sur trois points
    • Les syntaxes abrégées qui combinent des instructions réduisent la difficulté
    • Chaque écart par rapport au flux linéaire augmente la difficulté
    • Les flux de contrôle imbriqués augmentent la difficulté
  • Son nom peut être critiqué parce qu’il sonne comme une mesure scientifique ou objective, mais, en pratique, on peut y voir une heuristique efficace
  • Le problème de densité des syntaxes abrégées

    • Une syntaxe abrégée comme MyObj myObj = a?.myObj; est plus courte et demande moins de temps de lecture que if (a != null) { myObj = a.myObj; }
    • Cependant, ces deux morceaux de code peuvent ne pas être totalement équivalents en pratique
    • Dans le premier, myObj devient a.myObj ou null
    • Dans le second, myObj devient a.myObj ou undefined
    • Même les langages à typage fort comme TypeScript ou Rust ne font que réduire la probabilité d’oubli ; ils ne garantissent pas que tous les cas soient correctement traités
    • Avec un langage comme JavaScript standard, où la vérification de types est limitée, ces cas limites ont davantage de chances de ne pas être traités
    • Les syntaxes abrégées peuvent être faciles à écrire et à lire, mais il existe un compromis entre concision et densité
  • Éléments qui rompent le flux linéaire

    • Un code linéaire sans condition est plus facile à parcourir qu’un code avec conditions
    • La Cognitive Complexity considère comme facteurs d’augmentation de la difficulté non seulement les conditions, les boucles et goto, mais aussi les macros conditionnelles, try/except, les séquences d’opérateurs logiques et la récursion
    • switch est compté comme un seul groupe, tandis qu’une chaîne de else-if est considérée comme plus difficile à chaque else-if supplémentaire
    • Cela tient au fait que chaque branche else-if peut effectuer deux comparaisons ou plus
    • Toutefois, le fall-through d’un switch et les break manquants peuvent aussi accroître la difficulté de lecture
    • Les cas où l’on enchaîne le même opérateur logique dans une condition et ceux où l’on mélange &&, || et ! présentent des niveaux de difficulté différents
    • debug || verbose || consoleMode est une condition simple
    • debug || (verbose && consoleMode) se lit plus difficilement à cause du mélange d’opérateurs
    • debug || !(verbose && consoleMode) devient encore plus complexe avec l’ajout de la négation
  • Exceptions et goto

    • try/catch augmente la difficulté dans la Cognitive Complexity, mais plusieurs blocs catch ne sont pas considérés comme plus difficiles qu’un seul catch, tandis que try et finally sont ignorés
    • Le simple fait de lancer une exception peut aussi créer un coût de lecture
    • Lorsque la gestion des exceptions franchit les frontières d’une fonction, la complexité des fonctions concernées s’entremêle
    • Le lecteur doit chercher où cette exception est interceptée
    • goto est généralement compté comme un élément qui augmente la difficulté
    • Cependant, certains experts estiment qu’une forme comme goto out ou goto done, qui libère des ressources dans un cas d’erreur avant de quitter la fonction, peut être utile
    • À l’inverse, un goto qui franchit les limites d’une boucle d’une manière impossible à exprimer avec continue ou break impose au lecteur de reconstruire un nouveau flux de contrôle, ce qui représente une charge de lecture importante

Imbrication et forme des fonctions

  • Si une condition est en soi difficile à lire, une condition imbriquée l’est encore davantage
  • La Cognitive Complexity ajoute, au score propre des conditions et boucles, une difficulté supplémentaire pour chaque niveau d’imbrication
  • Cette idée est aussi appelée “Level of Indentation” ou “Bumpy Road”
  • Lorsque l’imbrication dépasse deux niveaux, la lecture devient particulièrement difficile ; un code qui réduit l’imbrication grâce à des retours anticipés se lit de manière plus plate
  • Cette métrique ne reflète pas directement la longueur d’une fonction, mais, toutes choses égales par ailleurs, une fonction longue demande plus d’effort de lecture qu’une fonction courte

Noms de variables, durée de vie et motifs familiers

  • Des noms distincts et explicites

    • Les noms explicites sont importants pour comprendre ce que le code cherche à faire, tandis que les noms redondants ou cryptiques produisent l’effet inverse
    • Le masquage de variables (variable shadowing) est dangereux
    • Il faut éviter les situations où le lecteur doit raisonner sur les règles de portée pour distinguer quelle variable est utilisée
    • Il faut aussi éviter les identifiants visuellement similaires
    • Des noms faciles à confondre visuellement, comme i et j, ou item et items, peuvent provoquer des erreurs
    • Un code qui utilise dans une même fonction plusieurs variantes d’un même nom de variable, comme node, _node, thisNode, est une forme propice aux bugs
  • Durée de vie courte des variables

    • L’analyse des variables vivantes (live variable analysis) considère la plage qui va du premier endroit où une variable est utilisée jusqu’au dernier endroit où elle peut l’être
    • Lorsque la durée de vie d’une variable est longue, le lecteur doit garder davantage de variables et de valeurs possibles en tête
    • Déclarer les variables juste avant leur utilisation effective peut réduire leur durée de vie, par rapport à une déclaration de toutes les variables en haut de la fonction
    • Le pire cas est celui d’une variable qui vit à travers plusieurs fonctions et est utilisée à plusieurs endroits
    • Si plusieurs variables ont en pratique la même durée de vie, un objet peut être plus approprié
    • Si un objet ne convient pas, il vaut mieux minimiser le nombre de fonctions et de lignes que le lecteur doit lire pour comprendre les valeurs
  • Longues chaînes et variables intermédiaires

    • Le style de programmation fonctionnelle raccourcit la durée de vie des variables, mais des chaînes trop longues ou une imbrication de callbacks peuvent créer une charge de lecture
    • Découper les longues chaînes de fonctions en petits groupes, et utiliser des variables intermédiaires ou des fonctions d’aide bien nommées, peut réduire la charge cognitive du lecteur
    • La version avec variables intermédiaires peut être légèrement moins efficace
    • Tant qu’un outil de performance n’indique pas que cette ligne constitue réellement un goulot d’étranglement, ces micro-différences d’efficacité n’ont pas d’importance
  • Réutilisation de motifs de code familiers

    • Réutiliser des formes de code et de variables familières permet au lecteur de reconnaître des motifs qu’il connaît déjà, ce qui rend la lecture moins fatigante
    • Cela rejoint le Principle of Least Surprise
    • Dans une codebase, il vaut mieux conserver de manière cohérente les formes récurrentes, comme la manière d’écrire les conditions
    • Lorsqu’il faut s’écarter d’un motif, les noms de variables ou les commentaires peuvent mettre en évidence la différence
    • En poussant cette idée jusqu’au bout, on en vient à utiliser des fonctions template ou génériques pour que le lecteur n’ait pas à reconnaître à nouveau des motifs répétés

8 motifs visuels pour améliorer la lisibilité

  • Line/Operator/Operand count : des fonctions plus petites et moins de variables et d’opérateurs sont plus faciles à lire
  • Novelty : éviter la nouveauté dans la forme des fonctions, les opérateurs et le sucre syntaxique, et réutiliser les motifs communs de la codebase
  • Grouping : diviser les longues chaînes de fonctions, itérateurs et compréhensions en groupes logiques au moyen de fonctions d’aide ou de variables intermédiaires
  • Conditional simplicity : garder les conditions aussi courtes que possible et, dans une même condition, préférer une séquence du même opérateur logique plutôt que mélanger différents opérateurs
  • Gotos : ne pas utiliser goto, sauf lorsqu’il suit un motif particulier de gestion d’erreur et que les alternatives sont moins bonnes
  • Nesting : minimiser la logique imbriquée et les grands changements d’indentation ; si une imbrication profonde est nécessaire, l’extraire dans une fonction distincte plutôt que l’enfouir dans une grande fonction
  • Variable distinction : utiliser des noms de variables explicites et visuellement distincts, et éviter le masquage de variables
  • Variable liveness : garder des durées de vie de variables courtes, en faisant particulièrement attention aux longues durées de vie qui traversent les frontières de fonctions

Problèmes observés dans une vraie codebase

  • Les codebases qui généraient une forte fatigue mentale combinaient plusieurs anti-patterns
  • Concrètement, elles comportaient de longues fonctions, un mélange de nombreuses constructions du langage et beaucoup de chaînes de fonctions qui auraient dû être séparées dans des fonctions d’aide
  • Résultat : dans de grandes fonctions, la complexité d’imbrication et les longues durées de vie des variables augmentaient ensemble
  • Malgré la grande qualité du code et de ses auteurs, au moins un bug critique a été découvert
  • L’un d’eux était un bug facile à voir, mais il était probablement passé inaperçu parce qu’il se trouvait au milieu d’une fonction longue et complexe, ce qui le rendait difficile à raisonner
  • Dans un mois, la personne la plus susceptible de relire votre code sera vous-même ; écrire sous une forme facile à lire est donc directement lié aux coûts pratiques du travail

1 commentaires

 
GN⁺ 2025-03-12
Avis de Hacker News
  • L’affirmation de l’article selon laquelle des chaînes map/reduce/filter longues ou multiples nuisent à la lisibilité ne découle absolument pas du reste
    On dirait une critique courante glissée discrètement parce que ce n’est pas familier, alors qu’avec un peu d’habitude, c’est souvent bien plus facile à lire et à écrire que les alternatives
    Par exemple, je vois mal quel code serait plus lisible que books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()
    Même d’après les métriques de complexité, ce genre de code est meilleur que presque n’importe quelle autre approche, et il faut apprendre au moins les bases de la programmation fonctionnelle. Pas besoin d’aller jusqu’à expliquer les monades, mais il faut être assez à l’aise pour ne pas dénigrer aveuglément map et filter

    • La formulation paraît inutilement agressive, mais l’exemple visé par la phrase citée n’était pas ce genre de chaîne courte
      Selon moi, cela commence à devenir difficile à lire à partir d’une chaîne d’environ 5 appels, et l’exemple de l’article était de cette longueur
      « multiple » désignait les cas où il y a des chaînes imbriquées ou où le type manipulé change, ce qui ralentit la lecture
      Les composants fonctionnels peuvent être élégants, mais on peut aussi en abuser
    • L’exemple n’est qu’un filtre conceptuellement simple sur une seule liste
      Quand la chaîne devient trop longue, que les conditions se complexifient et qu’il y a beaucoup de listes/variables, cela devient difficile à comprendre d’un seul coup
      Dans une boucle procédurale, on peut donner un nom aux résultats intermédiaires, et ce nom permet d’oublier le traitement précédent pour se concentrer sur l’étape suivante
    • SELECT DISTINCT author FROM books WHERE pageCount > 1000;
    • L’exemple est correct, mais la critique initiale visait plutôt des chaînes très longues, à mon avis
      Personnellement, j’ai tendance à découper les chaînes pour donner un nom aux résultats intermédiaires, comme si les noms de variables servaient de commentaires
      var longBooks = books.filter(book => book.pageCount > 1000)
      var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
    • Les solutions SQL proposées par les gens sont bonnes aussi, mais en Prolog on pourrait écrire ceci
      ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
      Selon la structure de la base de données Prolog, cela peut même être plus court
      ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
  • Je pense qu’un bon code comporte fondamentalement une part assez importante qualitative et littéraire
    Les programmeurs ou universitaires habitués à la pensée mathématique, qui veulent des réponses quantitatives, sont souvent mal à l’aise avec cet aspect
    J’aime à la fois Dostoïevski et Wodehouse : les deux sont excellents, mais de manière très différente
    Même si le code n’est pas un champ aussi ouvert, j’ai travaillé sur de bonnes bases de code qui donnaient des impressions qualitativement très différentes, et il faut souvent du temps pour « sentir » le style d’une codebase, comme lorsqu’on s’habitue au style d’un nouvel auteur

    • Je suis d’accord à 100 %. L’un des meilleurs compliments qu’on m’ait faits à propos de ma programmation était : « le code se lit comme une histoire »
      Cela voulait dire que les fonctions étaient ordonnées de façon à ce qu’en lisant le fichier de haut en bas, le récit se déroule naturellement, avec une implémentation déclarative qui semblait s’adresser au lecteur
      Je suis le paradigme de la programmation fonctionnelle pure, et je trouve qu’il se prête mieux à ce style plus narratif
      Les dépendances/entrées d’une fonction se limitent à ses arguments ou à d’autres fonctions pures, et sa sortie n’est contenue que dans son type de retour, ce qui facilite le guidage progressif à travers la complexité
      Contrairement à d’autres paradigmes qui comportent des complexités comme l’état caché, je pense ironiquement que le paradigme le plus mathématiquement précis est aussi celui qui convient le mieux à un style plus narratif
    • Si lire et comprendre l’objectif de haut niveau d’une fonction prend plus de 5 secondes, c’est du mauvais code selon moi
      Peu importe sa forme : si l’on ne peut pas comprendre ce que fait une fonction dans un délai raisonnable, sans longue expérience de développement, c’est tout simplement du mauvais code
    • Beaucoup de motifs syntaxiques considérés comme élégants me semblent en réalité moins clairs que les mathématiques elles-mêmes
      Par exemple, l’opérateur ternaire cité dans l’article, return n % 2 === 0 ? 'Even' : 'Odd';, se lit à l’envers pour le cerveau humain, et convient davantage au compilateur pour traiter l’arbre syntaxique qu’à une personne
      Un mathématicien humain écrirait plutôt une fonction définie par morceaux, du type : si n mod 2 = 0 alors 'Even', sinon 'Odd', ce qui est bien plus clair
    • C’est pour cela que les code reviews sont importantes
      Elles aident les nouveaux membres de l’équipe à assimiler un style cohérent, et maintiennent aussi le style de l’équipe dans une cohérence raisonnable
      Le commentaire sur .editorconfig mérite aussi d’être consulté : https://news.ycombinator.com/item?id=43333011
      Cela réduit les débats sur les détails de style dans les pull requests
    • C’est peut-être pour cela que le Literate Programming ne s’est jamais vraiment imposé
  • L’article est bon, mais je trouve qu’il passe à côté de l’élément le plus mentalement fatigant quand on lit du code : la mutabilité
    Pouvoir « figer » une seule fois le sens d’une variable en lisant une méthode, puis le conserver tel quel pendant qu’on déduit le reste, c’est un vrai cadeau
    La compréhension d’une méthode devrait progresser de façon monotone de 0 % à 100 %, sans qu’on ait besoin de redémarrer mentalement la méthode parce qu’on ne sait plus comment le corps de la boucle a modifié l’accumulateur à telle itération
    La vraie raison pour laquelle GOTO est nuisible est là aussi. Le problème n’est pas qu’il soit difficile de déplacer mentalement le pointeur d’instruction dans une méthode, mais qu’avec GOTO il devient difficile de connaître l’état des variables mutables

    • Je ne suis pas d’accord. Il existe un espace d’information abstrait que le code modélise, et le pointeur d’instruction mental doit se déplacer dans cet espace
      Les variables mutables comme immuables peuvent aider ou gêner ce déplacement ; tout dépend de la netteté avec laquelle le code correspond à cet espace
      Les variables immuables ont un petit avantage tactique, puisqu’on n’a pas à craindre que leur valeur change ou change d’une manière trompeuse, mais d’après mon expérience ce n’est pas assez important pour en faire une règle du type « utilisez toujours l’immutabilité »
      Parfois, la mutabilité permet de représenter cet espace d’information de façon beaucoup plus propre
    • La complexité totale ne se résume pas au problème de déplacer le pointeur d’instruction depuis un point de départ connu
      Du point de vue de l’appelé, et non du site d’appel, si quelqu’un peut sauter vers une ligne donnée, on ne peut pas retracer ce qui s’est passé avant. Comme cela peut venir de n’importe où, il faut une analyse globale du programme, pas une analyse locale
      Si la mutabilité était la vraie source de la complexité de GOTO, les instructions if et les boucles for devraient poser le même problème
      Je suis d’accord pour dire que la mutabilité et l’état créent directement de la complexité, mais je pense que GOTO appartient à une catégorie complètement différente et bien plus nuisible
  • Un pattern que je n’aime personnellement pas est celui qui consiste à retourner directement dans un if et à laisser le reste comme chemin par défaut implicite
    if (n % 2 === 0) return "Even"; return "Odd"; est certes plus court, mais je préfère largement if ... return "Even"; else return "Odd";
    La raison est que le premier donne une impression d’asymétrie. "Even" et "Odd" sont des choix symétriques, donc la version avec else est plus intuitive

    • L’écrire comme return (n % 2 === 0) ? "Even" : "Odd"; est ce qu’il y a de plus lisible, parce que c’est celle qui contient le moins de boilerplate
      Dans un langage doté d’un opérateur ternaire, cela devrait être facile à reconnaître
    • Les clauses de garde/retours anticipés ont tendance à déplacer l’attention du développeur non pas vers le chemin normal prévu, mais vers le rétrécissement progressif du comportement de la fonction
      D’expérience, else ajoute de l’imbrication et pousse facilement à continuer d’évaluer des conditions limites ou des variables au-delà de la portée immédiate du chemin normal. Il faut garder tout ce contexte en tête
    • Personnellement, je préfère la première approche
      On voit visuellement le retour avec un seul niveau d’indentation juste sous le nom de la fonction, et s’il n’y a pas de sortie anticipée, cela donne l’impression qu’un résultat est garanti
      Quand le retour est enfoui plus bas, je trouve ça un peu bizarre
    • J’ajouterais une ligne vide pour que return "Odd"; soit visuellement séparé du if, et si le langage le permet, j’ajouterais aussi des accolades au corps du if
      Il y a des cas où else se justifie, mais c’est généralement lorsqu’il y a des effets de bord ; le plus souvent, refactorer jusqu’à le supprimer rend le code plus clair
      Il est courant qu’un code complexe se transforme en séquence de gardes qui sortent par ordre d’importance ou de coût d’exécution, ce qui sépare la logique réelle de la fonction/méthode des conditions de terminaison
    • L’asymétrie devient évidente si l’on refactorise le code en style continuation/callback ou vers une mutation de structure de données plus complexe
      La première approche s’écoule vers le bas et exécute le second ensemble d’instructions. return est un opérateur spécial qui interrompt le flot de contrôle ; le flot de contrôle ordinaire de la première approche ne représente donc pas correctement la complétude des deux cas
      En Rust idiomatique, on n’utilise pas return sauf dans les cas exceptionnels qui rompent le flot de contrôle d’une méthode, et le second exemple apparaît généralement plus souvent sans instruction de retour
      En Python aussi, on fait généralement des retours anticipés au début en cas d’arguments ou d’état invalides, puis le retour en position finale devient la vraie valeur de retour
      À cause de ces conventions, casser une structure if-else complète donne à un return indenté l’apparence d’une situation exceptionnelle. Si l’on suit cette convention, les instructions return finissent naturellement par sembler redondantes sauf lorsqu’elles interrompent le flot de contrôle, et la convention de Rust devient compréhensible. Dans tous les langages, return est une instruction équivalente à break
  • Peut-être que je suis le seul, mais TypeScript rend parfois le code difficile à lire
    Tant qu’on garde le modèle de données relativement « atomique » et que les développeurs déclarent réellement les types et les documentent avec sérieux, ça va
    Mais dès que des types commencent à être dérivés d’autres types via des types utilitaires, et qu’on omet les types explicites pour s’appuyer sur l’inférence, les choses se défont rapidement
    Dans une pile profonde, avec 4 ou 5 niveaux d’indirection de types, il devient très difficile de suivre l’origine d’un champ. Certains sont inférés, d’autres explicites, certains sont des types dérivés, et des alias de champs s’y mêlent aussi
    Avec de grands modèles de données et des piles d’appels profondes, une forme comme function checkDogs(dogs: Dog[]) { ... } sans type de retour explicite devient totalement inutilisable et vraiment exaspérante

    • Je pense que les fonctions devraient probablement déclarer leur type de sortie
      La raison principale est de forcer tous les chemins de retour de cette fonction à respecter ce type
      J’ai vu beaucoup de régressions causées par l’ajout d’une nouvelle condition qui renvoyait un type légèrement différent de celui des autres branches
      En revanche, je ne vois pas beaucoup d’intérêt à annoter les déclarations de variables
      Dans l’exemple, const checkedDoggos = checkDogs([]) est très bien, et il suffit de laisser checkedDoggos hériter du type de la fonction
      Je travaille sur une base de code où le linter impose const checkedDoggos: DogBreedAndSize[] = checkDogs([]), et c’est assez ridicule, avec peu de valeur ajoutée
    • Même si TypeScript infère une partie des types de retour, je préfère avoir au moins un peu d’information de type plutôt qu’aucune, comme en JavaScript
      En JavaScript, on ne peut pas être sûr, donc il faut sans cesse monter et descendre dans la pile et garder tout ça en tête
    • Ma règle est de n’ajouter des types que lorsque le compilateur TypeScript se met à crier
  • Il faut se méfier de l’affirmation selon laquelle « les petites fonctions avec peu de variables sont généralement plus faciles à lire »
    Je n’aime pas que les discussions sur la lisibilité se concentrent uniquement sur la lisibilité microscopique. Cela conduit facilement à découper le code de façon excessive, sous l’hypothèse erronée que la lisibilité microscopique serait plus importante que la lisibilité macroscopique
    Ce genre de dogmatisme produit des programmeurs qui voient les arbres sans voir la forêt, et engendre du code excessivement inefficace ou difficile à déboguer
    Les langages de la famille APL sont à l’extrême opposé, mais le véritable optimum se situe probablement quelque part au milieu, et varie sans doute beaucoup selon les personnes

    • Il existe clairement un juste milieu, surtout quand plusieurs fichiers sont impliqués
      Dans du code qu’on ne connaît pas, devoir aller à la définition 3 ou 4 fois suffit déjà à devenir pénible ; c’est peut-être moi, mais j’ai du mal à imaginer que la plupart des gens s’en sortent beaucoup mieux que moi
      Dans la culture .NET, en particulier autour de la « clean architecture », ce problème atteint un niveau choquant. Quand on veut modifier une fonctionnalité ou remonter la piste d’un problème, c’est réparti sur 4 couches et 15 fichiers, dont certains sont composés à plus de 60 % de mots-clés
      Je ne sais pas où tracer la limite, mais je préfère en général une longue fonction qui suit les autres bonnes pratiques et se lit séquentiellement, plutôt qu’un code tellement fragmenté qu’il faut faire défiler vers le haut et vers le bas toutes les 5 lignes
      C’est pareil pour les types/classes : inutile de mettre dans un autre fichier un enum de 4 valeurs qui n’est utilisé que dans ce DTO
  • Article intéressant, mais pas entièrement satisfaisant
    Il saute trop vite aux conclusions et revient à la préférence personnelle. Je suis d’accord avec plusieurs préférences, mais l’article lui-même cherchait explicitement à dépasser les préférences
    Dire que « les opérateurs propres à un langage ou le sucre syntaxique font peser une charge sur le lecteur et doivent donc être évités » ne découle pas des métriques. Si une fonction contient 3 opérateurs différents, et qu’un opérateur propre au langage peut remplacer les trois d’un coup, alors « l’effort » de la fonction diminue
    Des composants comme map/reduce/filter, bien utilisés, peuvent aussi remplacer d’autres opérateurs et réduire le « volume » ; cela peut donc aller dans un sens comme dans l’autre
    L’exemple de ?. semble être un diagnostic très spécifique à JavaScript, qui pointe surtout une conception du langage difficile à lire. Dans beaucoup de langages, null et undefined ne sont pas séparés, et on parle souvent de null-safe operator
    « Le masquage de variables (variable shadowing) est horrible » et « une durée de vie plus longue oblige à garder davantage de variables en tête » peuvent entrer en conflit
    Dans certains contextes, j’aime beaucoup le masquage de variables, parce qu’il retire l’ancienne instance du scope au lieu de la laisser accessible en permanence

  • Il existe un excellent plugin pour VS Code appelé Highlight
    Il permet d’appliquer différentes couleurs au code avec des expressions régulières personnalisées, et l’usage courant est sans doute de mettre //TODO en jaune
    Je m’en sers pour estomper les logs, parce qu’à force d’en mettre un peu partout, ils génèrent beaucoup de bruit visuel
    La bibliothèque que je maintiens utilise des logs comme this.logger?.info('Some logs here');, et j’y applique une opacité de 0,4 pour les repousser à l’arrière-plan
    Ils restent visibles, mais au premier coup d’œil, la logique métier ressort davantage
    La configuration peut être adaptée comme suit : "highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }
    https://marketplace.visualstudio.com/items?itemName=fabiospa...

  • Je ne suis pas d’accord avec l’idée selon laquelle découper de longues chaînes de fonctions ou de callbacks en petits groupes avec des variables bien nommées serait un peu moins efficace
    Les deux versions peuvent être tout aussi efficaces
    Dans les deux cas, les mêmes objets sont alloués, stockés sur le tas et soumis au ramasse-miettes. La différence d’efficacité dépend du compilateur
    Dans la deuxième version, le compilateur devrait pouvoir voir que chaque variable n’est utilisée qu’immédiatement après sa déclaration, et traiter ces objets comme hors scope, comme dans un appel chaîné

    • D’accord. Après compilation, le compilateur ne se soucie probablement pas du fait qu’un nom ait été donné à la valeur de retour
      À condition, bien sûr, de laisser le type de la variable être inféré
      Le coût qu’on observe réellement apparaît quand on matérialise explicitement les valeurs intermédiaires pour les voir dans le débogueur. Par exemple, les transformer en liste introduit des allocations évitables, donc un coût
  • Vouloir quantifier la « lisibilité » est une bonne chose. Il nous faut beaucoup plus d’approches de ce genre
    Aujourd’hui, la définition la plus courante de la lisibilité ressemble surtout à « ce que moi je trouve facile à lire »
    On pourrait peut-être découvrir les dimensions réelles de la lisibilité en montrant du code à un très grand nombre de personnes, en leur demandant de choisir une phrase décrivant ce que fait ce code, et en chronométrant le tout
    Les exercices que le plus grand nombre de personnes réussissent avec le temps moyen le plus court deviendraient des exemples de code lisible dans le monde réel et, plus important encore, aideraient à identifier les pratiques réellement difficiles à lire
    Les répondants se regrouperaient probablement selon des axes comme « expérience en programmation » ou « compréhension du paradigme X », et les résultats pourraient évoluer avec le temps à mesure que les modes changent

    • L’une des difficultés centrales est que nous apprenons à lire le code
      Ce qu’on a appris à lire et à écrire façonne ce que l’on perçoit comme facile à lire
      Ce qu’on essaie de faire, les personnes avec qui l’on travaille, ce que l’on savait faire avant de coder, les autres langages que l’on connaît, etc., influencent beaucoup les choses
      Une fois cueillis les fruits les plus bas — par exemple, au-delà du simple fait d’éviter de donner aux variables des noms arbitraires, sans rapport ou trompeurs —, beaucoup de problèmes de « lisibilité » pourraient finalement relever de la construction d’un consensus
      Il n’existe peut-être pas de bonne réponse qui transcende le groupe précis de programmeurs avec lequel on cherche à travailler
    • Je pense que cela n’a pas grande valeur
      La lisibilité du code ressemble à la lisibilité d’une langue : c’est surtout un problème pour les personnes qui ne connaissent pas bien cette langue, et cela se résout avec du temps
      Le vrai problème en programmation, c’est la complexité du code, et on ne peut pas l’évaluer uniquement avec des métriques portant sur des fragments de code isolés
      Le problème se situe dans les relations entre les fonctions, plus que dans les choix d’implémentation à l’intérieur du corps d’une fonction
    • Cette approche est trop unidimensionnelle
      Comprendre ce que fait le code est généralement facile ; ce qui est difficile, c’est de le modifier ou d’ajouter une fonctionnalité
      Cette difficulté vient du fait que plusieurs couches d’abstraction masquent la manière dont elles sont reliées entre elles