1 points par GN⁺ 2023-11-16 | 1 commentaires | Partager sur WhatsApp
  • Deux règles de structuration du code sont au cœur de l’idée : faire remonter les branchements conditionnels vers l’appelant, et pousser le traitement itératif vers les opérations par lot
  • Faire remonter les if facilite l’imposition des préconditions via des types ou des assertions, et permet de regrouper les flux de contrôle complexes en un seul endroit pour repérer plus facilement les conditions dupliquées et les branches mortes
  • Une structure qui crée un enum pour ensuite refaire immédiatement un match répète la même condition sous forme de branchement, de structure de données puis de rebranchement ; on peut alors souvent la simplifier en appels à foo(x) et bar(y)
  • Faire descendre les for fait des opérations par lot l’unité de base plutôt que l’itération sur des objets individuels, ce qui permet d’amortir les coûts de démarrage, de réorganiser l’ordre de traitement, et d’ouvrir la voie à la vectorisation et aux optimisations en struct-of-arrays
  • Sortir les conditions de la boucle permet de réduire les branchements dans les boucles chaudes, et rejoint des architectures comme TigerBeetle, qui amortissent le coût décisionnel du plan de contrôle par un traitement par lots dans le plan de données

Faire remonter les if

  • Les conditions if à l’intérieur d’une fonction sont d’abord des candidates à déplacer vers l’appelant
    • L’exemple présenté considère qu’une fonction qui reçoit directement un Walrus est préférable à une fonction qui reçoit un Option<Walrus> et retourne si la valeur est None
    • Plutôt que de vérifier la précondition dans la fonction puis de « ne rien faire », il est plus clair que l’appelant fasse la vérification et que le type ou une assertion garantisse sa validité
  • Ce refactoring, qui consiste à faire remonter la vérification des préconditions, peut se propager le long du chemin d’appel et réduire au final le nombre total de vérifications
  • Le flux de contrôle et les if augmentent la complexité et sont souvent source de bugs
    • Faire remonter les if concentre la logique de branchement complexe dans une seule fonction, tandis que le travail effectif descend dans des sous-routines linéaires
    • Un flux de contrôle complexe est plus facile à analyser pour détecter duplications et conditions mortes lorsqu’il tient dans une seule fonction à l’écran plutôt que d’être dispersé dans tout le fichier
  • Le refactoring « dissolving enum » consiste à éliminer le motif où un branchement est transformé en structure de données, puis immédiatement retransformé en branchement
    • Si f() crée E::Foo(x) ou E::Bar(y) selon une condition, puis g(e) refait un match pour appeler foo(x) ou bar(y), la même condition se répète sous plusieurs formes
    • En faisant remonter la condition dans main(), on obtient une forme plus simple : if condition { foo(x) } else { bar(y) }

Faire descendre les for

  • Dans une perspective orientée données, les programmes et les hot paths manipulent généralement de nombreux objets ; on introduit donc des lots d’objets et on prend les opérations par lot comme cas de base
    • La version scalaire pour un objet individuel devient un cas particulier de l’opération par lot
    • for walrus in walruses { frobnicate(walrus) } est une moins bonne forme que frobnicate_batch(walruses)
  • Le principal avantage est la performance
    • Traiter tout un lot d’un coup permet d’amortir les coûts de démarrage
    • On peut réorganiser plus librement l’ordre du traitement, et adopter une vectorisation ou une approche struct-of-arrays où l’on traite d’abord un même champ pour toutes les entités
    • Comme cas extrême, l’article renvoie à Vectorized Interpreters Talk
  • Un exemple intéressant cité est la multiplication de polynômes basée sur la FFT
    • Évaluer un polynôme simultanément en plusieurs points peut être plus rapide que de refaire plusieurs évaluations point par point
  • Les règles sur les if et les for peuvent s’appliquer ensemble
    • La bonne forme consiste à brancher en dehors du if condition, puis à exécuter for walrus in walruses dans chaque branche
    • La mauvaise forme consiste à réévaluer if condition à chaque itération à l’intérieur de la boucle for
    • Cela évite les réévaluations inutiles de la condition, supprime les branchements dans les boucles chaudes et peut rendre la vectorisation possible
  • Ce motif s’applique aussi bien au niveau micro qu’au niveau macro
    • L’architecture de TigerBeetle amortit le coût décisionnel du plan de contrôle en traitant simultanément des lots d’objets dans le plan de données
    • Si la motivation première du conseil sur les for est la performance, une approche par collections comme avec jQuery peut aussi améliorer l’expressivité

1 commentaires

 
GN⁺ 2023-11-16
Avis sur Hacker News
  • Au début, j’ai été surpris que cet article suscite autant de réactions hostiles, mais en me rappelant qu’il s’agit de conseils de conception orientée données, ça devient compréhensible
    Comme beaucoup de gens sur ce forum, je développe surtout des applications web professionnelles, et dans ce contexte ce conseil peut passer pour n’importe quoi
    Si, dans votre travail quotidien, vous n’avez pas à vous soucier du cache d’instructions, l’heuristique selon laquelle on peut globalement ignorer ce conseil semble correcte
    Si vous voulez sentir dans quels cas ce conseil devient important, cherchez “Typical C++ Bullshit” de Mike Acton ; cet article donne l’impression d’en proposer une synthèse plus facile à comprendre
    Je partage assez largement les préoccupations de Casey Muratori, mais la plupart des logiciels professionnels doivent optimiser la facilité de modification et la correction plutôt que les performances, autrement dit la “programmation dans la durée”

    • Je pense que ce conseil reste tout à fait applicable à la programmation orientée objet
      Les développeurs essaient souvent de rendre du code métier complexe DRY en le découpant en petites méthodes private dans des classes ; “remonter les if” permet d’éviter que la logique de branchement soit éparpillée entre plusieurs méthodes
      “Descendre les for” est tout aussi important. De nombreux flux d’appels finissent par une requête de base de données coûteuse, et il est fréquent qu’une boucle située plus haut provoque plusieurs appels à la DB plus bas
      Cette itération peut souvent être remplacée par une clause where ou un join SQL, et pour les agrégations ou le filtrage, il vaut mieux les pousser au plus près du DAO plutôt que de sortir une foule d’objets pour itérer dessus, car cela se prête mieux à l’optimisation près de la base de données
      Cela dit, comme pour tout principe de conception, il ne faut pas l’appliquer comme un dogme, mais juger consciemment
    • La conception orientée données semble toujours provoquer les gens. Probablement parce qu’elle implique que beaucoup d’éléments de l’approche orientée objet dominante sont erronés
      Je trouve regrettable d’opposer facilité de modification et performances. En pratique, je n’ai pas encore vu de preuve convaincante qu’elles entrent réellement en conflit
    • Facilité de modification, correction et performances ne sont pas mutuellement exclusives ; elles vont même souvent de pair
      L’un des aspects les plus importants des performances est de faire petit. Du code compact, de petites structures de données, peu d’instructions exécutées : c’est essentiel, et “penser au cache d’instructions” revient fondamentalement à écrire du petit code
      Plus le code est petit, moins il y a de place pour les bugs, plus il est facile à comprendre d’un seul coup, et plus il est facile d’obtenir une bonne couverture de tests, ce qui favorise la correction. La facilité de modification y gagne aussi, car plus le code est petit, plus les changements sont petits
      Bien sûr, certaines optimisations rendent le code plus complexe, comme la parallélisation, le caching ou les optimisations bas niveau, mais ce n’est qu’une partie de l’optimisation des performances ; un programmeur qui se soucie sérieusement des performances ne ferait pas cela sans profiling ni analyse
      if/for et for/if peuvent être très proches fonctionnellement tout en ayant des performances différentes ; si l’on peut choisir correctement sans coût supplémentaire, il n’y a pas de raison de choisir délibérément la version plus lente
    • Si l’on écoute John Ousterhout, en pratique, facilité de modification et correction n’entrent pas autant en conflit avec les performances qu’on pourrait le penser au départ
      Les programmes simples ont une pile d’appels courte et évitent les abstractions complexes et coûteuses ; ils ont donc tendance à utiliser moins de mémoire et à tourner plus vite
      Chercher des performances de tout premier niveau peut rendre un programme complexe, mais la vraie simplicité apporte le plus souvent des performances raisonnables
      “Descendre les for” relève surtout d’un conseil orienté données, tandis que “remonter les if” se rapproche davantage d’une façon de rendre le programme plus simple. Plus précisément, il s’agit d’améliorer la localité du code source, et l’idée centrale est de regrouper la logique de branchement au même endroit : https://loup-vaillant.fr/articles/source-of-readability
    • Je pense que ces deux conseils s’appliquent aussi assez bien aux applications professionnelles
      En particulier, “descendre les boucles” est puissant dans les applications CRUD. Traiter les créations et les mises à jour par lots autant que possible fait souvent gagner bien plus de temps que les goulots d’étranglement CPU
      La différence entre items.map(insertToDb/postToServer) et insertToDb/postToServer(items) se mesure presque toujours en plusieurs ordres de grandeur
      J’ai vu ce type d’optimisation faire passer des traitements de l’ordre de la seconde ou de la minute à la milliseconde, et souvent rendre aussi l’API plus propre et les logs plus faciles à lire
  • Avec l’expérience, j’ai l’impression que beaucoup trop de programmeurs se soucient des petits morceaux de « joli code », mais pas assez de la conception de l’ensemble de la codebase.
    Des fonctions concises et bien polies, c’est très bien, mais ce genre d’article peut dégénérer, dans les PR ou les discussions, en débats stériles de type « abri à vélos ».
    Qu’une fonction soit un peu désordonnée, où l’on place les if et les for, ou qu’on utilise map et filter, tout cela m’importe assez peu si le nom de la fonction est approprié, si l’interface et les types sont expressifs, si l’objectif est clair, si c’est documenté et si les effets de bord ne sont pas utilisés à l’excès.

    • « Remonter les if » n’est pas un débat d’abri à vélos ; c’est plutôt, comme vous le dites, un choix d’architecture à trancher.
      Si l’on réfléchit à l’endroit où valider une entrée, cette heuristique dit qu’il faut le faire au niveau le plus haut où l’entrée est reçue.
      Cela aide aussi à comprendre le code et, du point de vue de la preuve, c’est techniquement nécessaire, car les préconditions doivent être propagées vers le haut.
      Le premier conseil n’est clairement pas un débat anodin ; le second est un peu plus ambigu.
    • En travaillant dans la sécurité, j’ai côtoyé beaucoup de tech leads et d’architectes d’un niveau à peu près L7 à L9 selon les critères FAANG, et les personnes les plus obsédées par la « bonne conception » ont généralement entre 5 et 10 ans d’expérience.
      Elles essaient d’éviter que les juniors ne se tirent une balle dans le pied, mais n’ont souvent pas encore une intuition suffisante du coût de la complexité ni de la façon dont le code évolue à long terme.
      À l’inverse, après plus de 20 ans de pratique, on finit par accorder plus de valeur à la simplicité qu’à la plupart des aspects techniques.
      Des questions comme « à quel moment bifurquer ? » ou « que doit réellement faire ce code ? » ont tendance à donner les réponses les plus précieuses sur le long terme.
      Les questions d’abstraction et d’encapsulation mènent plutôt au type de débat évoqué plus haut, et si l’on trouve le plus de problèmes de sécurité dans le code des gens qui ne voient que la « vue d’ensemble », c’est aussi parce qu’ils ne comprennent pas bien ce que fait réellement cette codebase pourtant bien conçue.
    • Si ces principes qui semblent mineurs sont définis à l’avance, on peut réduire le nombre de débats stériles du même genre dans les PR et les discussions.
      Dans les logiciels sensibles aux performances où la conception orientée données convient bien, il faut prêter davantage attention à ces détails de petite échelle, parce que les optimisations du compilateur fonctionnent de cette manière.
      Les règles du jeu changent alors : le sens des instructions devient important, le code auto-explicatif gagne en valeur, les commentaires de code ne restent utiles que pour le raisonnement, et la « documentation » se rapproche davantage d’une spécification et d’un manuel utilisateur.
    • J’ai aussi vu beaucoup l’inverse : des gens qui aiment concevoir des cathédrales surconçues, mais ne réfléchissent pas à l’efficacité bas niveau des algorithmes.
    • Une raison est que les langages de programmation manquent de moyens pour exprimer de façon significative des composants bien plus grands que des fonctions, ou des composants reliés entre eux autrement que comme des fonctions.
      Au mieux, on peut regrouper des fonctions, puis il faut se débrouiller.
  • L’inconvénient de « remonter les if » est que, dans la définition de la fonction, les préconditions et postconditions ne sont pas visibles directement, et qu’il faut les vérifier à chaque point d’appel.
    Dans un grand projet auquel participent plusieurs personnes, ce type de fonction peut être réutilisé hors de son contexte prévu et entraîner des bugs.
    On peut résoudre cela avec un framework de contrats, mais on finit par écrire les conditions deux fois, dans le contrat et dans le code ; les types dépendants posent le même problème.
    Une approche consistant à étiqueter des zones de code appartenant à un contexte donné, puis à définir des fonctions appelables uniquement dans ce contexte, est intéressante.
    En Python, on pourrait utiliser un décorateur comme @requires_context("VALIDATED_XY") et un context manager validated_xy pour n’autoriser l’appel de la fonction que dans une zone où la validation a été effectuée.
    Le runtime ne connaît pas la signification de ce contexte, mais avec l’outillage et les tests appropriés, on peut concevoir le système de manière à n’établir le contexte voulu que lorsque certaines conditions sont satisfaites.
    Dans des langages comme Haskell, on peut l’imposer au niveau des types avec quelque chose comme la monade identité ; et même sans contrainte au niveau des types, cela peut être une façon intéressante de protéger les zones de code « unsafe ».

    • Je pense que vous avez manqué la deuxième partie de l’argument de l’auteur : il s’agit de « pousser la vérification des préconditions vers l’appelant et de pouvoir les imposer par les types ».
      Les préconditions restent donc visibles directement dans la définition de la fonction, mais elles apparaissent dans la signature de type plutôt que sous forme d’instruction if.
      C’est un pattern courant en Rust, utilisé dans l’article, et contrairement à une vérification par if, c’est une précondition stricte vérifiée à la compilation plutôt qu’à l’exécution : si elle n’est pas satisfaite, le programme ne compile pas.
    • En Python aussi, on peut exprimer cela avec des types.
      Par exemple, on peut accepter do_something(position: ValidatedPosition), puis valider un Position ordinaire avant de le convertir en ValidatedPosition et de le passer à la fonction.
      En pratique, on encapsulerait probablement la validation dans le constructeur de ValidatedPosition, mais l’idée clé est que si l’on tente de passer directement un Position, mypy signalera qu’un mauvais type a été fourni.
      Le typage de Python n’est pas aussi complet que celui de Rust, mais il devient de plus en plus utile lorsqu’on veut garantir que les données transmises ont bien été traitées comme il faut.
    • Le conseil « remonter les if » ne porte pas sur la vérification des préconditions, mais sur le choix du bon chemin de code.
      Si une fonction a des préconditions, on peut évidemment les affirmer au début de la fonction. Par exemple, le système de types de Java autorise null, donc une fonction qui a besoin d’un objet doit lever une exception si elle reçoit null.
      Chaque point d’appel a la responsabilité de n’appeler la fonction que lorsque les préconditions sont vraies. C’est inhérent à la définition d’une précondition.
      Appeler une fonction en violant ses préconditions est un bug de l’appelant. Il peut être nécessaire d’ajouter dans la fonction du code qui vérifie cela pour éviter un comportement indéfini, mais il faut distinguer ces assertions du véritable flux de contrôle du programme ; l’article parle du second.
    • Dans l’exemple fn frobnicate(walrus: Walrus), si l’on passe autre chose qu’un Walrus possédé, le programme ne compile pas.
      Même avec des génériques, les arguments doivent satisfaire les bornes de traits, et selon la manière dont les arguments sont utilisés dans la fonction, le compilateur impose les bornes nécessaires à la définition de la fonction.
    • J’ai l’impression que l’intention de public et private était en partie de jouer ce rôle d’étiquetage du code selon un contexte donné.
      Ou alors il faudrait peut-être une sémantique plus précise, qui traverse les domaines couverts par public, private et protected dans l’écosystème .NET.
  • Le premier exemple n’est pas mauvais à cause du if et du for, mais pour une autre raison
    En général, quand on a un « conteneur » de quelque chose, il vaut mieux écrire les fonctions sur « l’objet » de niveau domaine qu’il contient, plutôt que sur le conteneur lui-même
    En Clojure, quand on utilise un agent, on n’écrit pas des fonctions pour l’agent, mais des fonctions pour l’objet que l’agent peut contenir
    En Elixir aussi, les fonctions cœur du domaine opèrent sur la structure de données métier interne, pas sur le PID, et les appels GenServer leur sont délégués si nécessaire
    Cela rend le code plus flexible et permet de séparer plus proprement le cœur du domaine — « frobnicate un Walrus » — de la préoccupation applicative — « il peut y avoir ou non un Walrus, et s’il y en a un, le frobnicate »

    • Le conseil donné semble risquer de faire remonter trop haut la logique de validation
      Valider tôt est une bonne chose, mais il est aussi important d’émettre clairement une erreur de validation, plutôt que de laisser une fonction exploser soudainement avec une erreur bizarre
      Haskell résout ce problème avec newtype, en fournissant une sorte de « conteneur transparent certifiant qu’une validation appropriée a déjà été faite »
      Le conseil que j’aimerais vraiment faire passer aux gens est de préférer les « if tristes ». Plutôt que d’imbriquer le chemin normal sur plusieurs niveaux, un code qui vérifie les conditions invalides une par une en haut, puis les corrige ou s’arrête immédiatement, est presque toujours plus lisible et plus maintenable
      Les humains ont naturellement tendance à se concentrer sur le cas attendu, mais le code, lui, demande au contraire de se concentrer d’abord sur les cas exceptionnels. Chaque if crée une charge mentale, et si l’on doit récupérer des informations auprès d’un système externe ou sortir tôt en erreur, on peut se débarrasser immédiatement de cette charge lorsque la détection et le traitement sont côte à côte
    • Si l’idée est que le premier exemple serait meilleur sous la forme walrus.frobnicate(), j’ai l’impression que c’est davantage une question de préférence syntaxique que le point central que l’auteur veut défendre
  • Je n’irais pas jusqu’à dire que c’est un mauvais conseil, mais ce n’est pas forcément un bon conseil non plus
    Le fait que le langage choisi soit Rust en dit assez long. Un système de types fort évite une grande partie de la programmation défensive nécessaire dans d’autres langages
    Si un programmeur C ne vérifie pas la validité d’un pointeur passé à une fonction et provoque un déréférencement de NULL, je n’ai pas envie de travailler dans la même équipe que lui
    Donc, au moins certains if doivent clairement se trouver plus bas, et il vaut mieux que les erreurs se propagent correctement vers le haut
    Je suis moins catégorique pour les for, mais en C, comme les arguments tableau se dégradent en pointeurs, je pense que les boucles doivent elles aussi être en haut plutôt qu’en bas. Dans la fonction où le tableau est créé, on peut faire confiance à sa longueur ; dans une fonction à laquelle il est passé en argument, non

    • Les interfaces en C doivent documenter soigneusement leurs attentes et ne vérifier que cela
      La documentation doit remplacer un système de types fort ; les vérifications à l’exécution ne doivent pas le faire
      Un code rempli de vérifications de NULL et d’autres mesures défensives est beaucoup plus difficile à lire
      On peut dire qu’il faut des vérifications plus défensives aux frontières des bibliothèques, et c’est exactement ce que cet article dit de faire remonter
      Le code critique pour la sécurité peut être différent, mais dans la plupart des cas, il suffit que les déréférencements accidentels de NULL soient détectés par les tests, les sanitizers et le fuzzing
    • En C, NULL est souvent une valeur de pointeur valide, mais non déréférençable
      Pour vérifier les valeurs de pointeur invalides, il faudrait tester l’ensemble des innombrables mauvaises valeurs possibles ; si l’on ne vérifie que NULL, on ne vérifie pas vraiment les valeurs invalides
      Si la précondition d’une fonction est « le paramètre p ne peut pas être NULL », alors on peut la vérifier. Mais si la précondition est « p doit être un pointeur valide », bonne chance pour trouver l’assertion appropriée
    • Mon heuristique est que si le système de types n’empêche pas les mauvaises valeurs, alors la responsabilité de les bloquer incombe au runtime
      J’utilise beaucoup T-SQL en ce moment, et on ne peut pas déclarer les paramètres ou les variables en NOT NULL
      Il vaut donc mieux vérifier NULL le plus tôt raisonnablement possible, généralement tout en haut de la procédure stockée
      Sinon, NULL peut se propager de façon inattendue profondément dans la hiérarchie d’appels et créer un problème moins clair
      Heureusement, les données de table peuvent être déclarées NOT NULL, donc ce genre de bug ne corrompt généralement pas les données, mais il est bien plus facile de l’attraper tôt
      Cela dit, s’il existe une logique qui écrit en base selon la valeur d’un paramètre, et que cette valeur est inopinément NULL, une mauvaise valeur peut être écrite, ou une valeur nécessaire peut ne pas l’être du tout, ce qui revient en pratique à corrompre les données
      La programmation défensive est donc la réponse
  • Sans contexte approprié, ce conseil paraît assez étrange, voire peut-être mauvais
    Les boucles et les instructions if sont toutes deux des opérations de flux de contrôle, donc une partie du raisonnement de l’article n’a pas beaucoup de sens
    L’argument le plus fort semble être la performance, mais la performance devrait généralement être la dernière chose dont on se préoccupe, surtout lorsqu’il s’agit d’un conseil heuristique
    Malheureusement, l’auteur en a fait un slogan, et j’espère qu’il ne se diffusera pas

    • La performance peut être un argument, mais ce n’est pas un argument fort
      Si l’on peut écrire le code amélioré comme dans l’exemple de l’auteur, la condition est constante pendant l’exécution de la boucle. Tant qu’on n’évalue pas à chaque fois une condition coûteuse, la prédiction de branchement s’en charge en grande partie
      Si la condition est une expression booléenne composée de valeurs const, le compilateur a aussi des chances de s’en rendre compte
  • Je pensais que tout l’article allait enchaîner sur un exemple de ce genre : répartir les walrus selon la condition, puis appeler frobnicate_batch(fwalrus) et transmogrify_batch(twalrus)
    Mais en réalité, l’exemple est parti sur un seul if qui enveloppe toute la boucle en deux branches

  • Je suis surpris que les programmeurs issus du génie logiciel se trompent si souvent là-dessus
    J’ai commencé à programmer dans le domaine scientifique, où il est absolument indispensable de réfléchir à ce genre de choses
    Il suffit de se tromper dans l’ordre des boucles pour qu’une simulation prenne une semaine au lieu d’une heure
    Grâce à ce bagage, j’ajuste instinctivement l’ordre des for et des if pour obtenir de petites optimisations, et le code qui ne le fait pas me semble tout simplement incorrect

  • Je ne suis pas sûr que ce genre de règle générale puisse vraiment s’appliquer au code réel.
    Ces règles ressemblent souvent à des dogmes mal placés, et même si l’article de blog commence par dire qu’il s’agit d’une heuristique, les jeunes programmeurs ne le prennent pas toujours ainsi.
    Il y a quelques semaines, YouTube n’arrêtait pas de me recommander une vidéo intitulée « I’m a never-nester », qui semblait défendre l’idée de ne jamais imbriquer de if, et c’était assez ridicule.
    Comme dans l’exemple de l’article, du code avec un if condition dans un for était marqué comme « mauvais », mais dans la plupart des codes réellement écrits ainsi, condition dépend de walrus, ce qui empêche de remonter le if.
    Si c’était possible, le fait de réévaluer la même condition à chaque itération serait tellement évident que les programmeurs auraient naturellement tendance à l’éviter.
    Mais si un junior ou un étudiant lit un conseil qui sonne comme un dogme, il peut essayer de le suivre strictement et produire un code encore pire.

    • Je ne suis pas particulièrement dogmatique sur le « ne jamais imbriquer », mais, à supposer que la syntaxe le permette, je n’ai jamais vu empiriquement de cas où séparer quatre cas avec match (condition_a, condition_b) serait moins bien que des if imbriqués.
    • Je vois plutôt cela comme un bon exemple de if qu’il faudrait « remonter ».
      Le but du code est d’effectuer une opération donnée sur walrus selon une condition, mais en réalité le if est utilisé à la place du polymorphisme et du système de types.
      Pourquoi walrus devrait-il avoir deux fonctions à appeler dans des scénarios différents ? Pourquoi ne pas avoir une seule fonction et deux types, puis passer le bon type ?
      Même avec la structure actuelle, on pourrait choisir la fonction de traitement plus haut selon la condition, puis faire en sorte que la boucle plus bas appelle à chaque fois la fonction sélectionnée.
      Si l’on prend la décision le plus tôt possible, on n’a pas besoin de la disséminer dans tout le code. L’intérieur du code effectue la même opération à chaque fois, sans branchement, et seule la sortie varie via le graphe de composition.
      Bien sûr, ce n’est pas une idée nouvelle, et elle était déjà ancienne il y a 15 ans : https://www.youtube.com/watch?v=4F72VULWFvc
    • Le refactoring « GOOD » ne fonctionne que lorsque la condition ne dépend pas de walrus, et il aide à rendre ce fait explicite.
      Si l’on réapplique ici « descendre le for », cela donne une forme où l’on appelle frobnicate_batch(walruses) ou transmogrify_batch(walruses) à l’intérieur de if condition.
    • Je trouve que c’est bien que ce genre d’articles existe.
      Quelqu’un a mis des mots sur un problème que j’avais rencontré plusieurs fois sans réussir à bien l’exprimer, et cela ressemble à un bon modèle à garder en tête.
      En même temps, ce genre de critique a aussi de la valeur. J’espère qu’un développeur junior dogmatique lira les deux et portera un jugement un peu plus nuancé.
    • L’essentiel à apprendre de toutes les « heuristiques » et « bonnes pratiques », c’est la raison qui les sous-tend.
      Les programmeurs, en particulier, ne devraient pas les copier et les appliquer mécaniquement.
      Appliquer aveuglément des heuristiques et des bonnes pratiques peut être une mauvaise idée, et au bout du compte, tout « dépend du contexte ».
  • Ce genre d’heuristique contient généralement une parcelle de sagesse, mais, le plus souvent, cela crée ensuite chez les programmeurs débutants quelque chose qu’il faudra désendoctriner.
    Il y a toujours beaucoup de cas où chercher à les suivre trop strictement aggrave les choses, et « savoir quand ne pas écouter ce conseil » est en fait la principale difficulté.

    • La désendoctrination sera de toute façon nécessaire.
      Je pense que ces règles sont utiles pour expérimenter. On peut les adopter, les pousser jusqu’au bout, les retourner pendant une journée ou un an, et voir jusqu’où cela mène.
      Apprendre leurs limites est aussi une couche de plus ajoutée au palimpseste qui ne cesse de se réécrire.
    • Cet article peut être utile comme koan au sein d’un recueil plus vaste.
      Certains de ces koans devraient se contredire les uns les autres.