- Dans les systèmes de très grande taille, qui atteignent des dizaines de millions de lignes, personne ne peut tout garder en tête ; les ingénieurs doivent donc pouvoir travailler efficacement avec une compréhension partiellement exacte
- Programming as Theory Building de Peter Naur considère que, lorsque la compréhension de l’équipe d’origine disparaît, il vaut mieux abandonner le programme et en créer un nouveau ; mais les grands systèmes, mêlant utilisateurs et innombrables exceptions, sont difficiles à reconstruire à partir de zéro
- Même une base de code dont tous les responsables sont partis peut être remise en état si l’on comprend d’abord un flux de bout en bout, puis que l’on élargit prudemment le périmètre des changements ; dans les grandes organisations, cette reconstruction de la compréhension se produit de manière répétée
- Les LLM freinent la formation de modèles mentaux détaillés, mais aident aussi à créer et exploiter rapidement une compréhension partielle ; la collaboration, les exigences légales, les mises à jour de sécurité et l’introduction de dépendances imposent également des compromis entre la compréhension du code et d’autres valeurs
- Un modèle mental exact rend le développement plus agréable et plus stable, mais ce n’est pas un critère absolu ; dans la pratique, il faut parfois renoncer à une compréhension complète au profit de la vitesse, de la conformité légale ou des besoins de l’organisation
Compréhension complète et compréhension partielle
- Dans les petites bases de code où les changements d’équipe sont rares, il est facile de penser qu’il faut tout comprendre pour bien travailler
- C’est le cas de projets comme Redis ou The Witness)
- Dans de grandes bases de code où les équipes changent souvent, comme le backend de Google Web Search ou GitHub, il est impossible de tout comprendre ; chacun travaille donc en essayant de saisir au mieux la zone locale dont il est responsable
- Ces deux environnements diffèrent fortement par leurs méthodes de développement, leurs pratiques et leur culture, mais dans les discussions en ligne sur le génie logiciel, la première culture, qui valorise la compréhension complète, est surreprésentée
- Les ingénieurs open source sont davantage incités à partager leur travail par écrit, et le travail d’ingénierie pur y ressort plus facilement que dans les grands systèmes propriétaires
- Les systèmes propriétaires sont difficiles à rendre publics pour des raisons juridiques et, même lorsque c’est possible, expliquer une grande base de code exige beaucoup trop de contexte concret
- Dans de nombreux environnements logiciels, la compréhension partielle n’est pas un état erroné ; dans les grands systèmes, c’est le meilleur résultat réalistement atteignable
- Cette différence mène au conflit entre culture de la compréhension complète et culture de la compréhension partielle décrit dans Pure and impure software engineering
La thèse de Programming as Theory Building
- L’article de Peter Naur, Programming as Theory Building, considère que le principal résultat produit par les programmeurs n’est pas le code, mais une théorie du programme
- Cette théorie est constituée d’une compréhension intuitive de ce qui se passe et pourquoi
- Le code et la documentation ne peuvent contenir qu’une partie de cette compréhension
- Même s’ils perdent le code, une équipe qui possède la théorie peut réécrire le programme ; mais si l’équipe est entièrement remplacée et que la compréhension est perdue, le code existant devient difficile à appréhender
- Selon Naur, comme il est impossible de reconstruire la théorie existante uniquement à partir de la documentation ou du code, il faut abandonner le programme d’origine et laisser une nouvelle équipe résoudre le problème depuis le début
- The Concept of Mind de Gilbert Ryle, cité par Naur, donne une portée plus large à la construction de théorie
- En pratique, une théorie ou un savoir-faire peut se former naturellement pendant que l’on accomplit quelque chose
- Le fait d’explorer le code lui-même pour comprendre une base de code existante est donc compatible avec cette approche
Pourquoi les grands systèmes ne peuvent pas être réécrits à partir de zéro
- Dans un système suffisamment grand et utilisé par des utilisateurs, des milliers de cas limites et comportements particuliers difficiles à réimplémenter s’accumulent
- Même une équipe qui connaît bien le système ne peut pas garder simultanément tous les détails en tête, ce qui rend difficile une réécriture complète en une seule fois
- Une réécriture réussie consiste à découper la base de code existante en petites parties isolées, puis à les remplacer une par une
- Au final, une réécriture revient elle aussi à appliquer une série de changements au système existant
- Si l’on ne peut pas modifier le système existant, il est encore plus difficile de le remplacer entièrement par un nouveau
Comment récupérer une base de code abandonnée
- Dans les entreprises technologiques qui possèdent des centaines de millions de lignes de code et des milliers d’ingénieurs, il n’est pas rare qu’il ne reste plus personne connaissant une base de code donnée
- Il suffit que quelques responsables quittent l’entreprise au mauvais moment, ou qu’une base de code ne soit pas maintenue pendant un an, pour en arriver là
- Naur pensait qu’il serait rare de confier à de nouveaux programmeurs la récupération d’un programme complètement mort, sans personne ayant ne serait-ce qu’une connaissance partielle de la théorie existante ; mais dans les grandes organisations, cela arrive réellement
- Même une base de code abandonnée peut, avec du temps, retrouver un état dans lequel il est possible de construire une nouvelle compréhension et de travailler efficacement
- Il faut d’abord comprendre un flux de traitement du début à la fin
- Puis, en effectuant prudemment des changements, élargir le périmètre de compréhension depuis ce flux vers les zones voisines
Tout le monde travaille avec une théorie imparfaite
- Les grands logiciels modernes sont si vastes qu’une personne, et même une équipe entière, ne peut pas garder tous leurs comportements en tête
- Dans une base de code suffisamment grande, tout le monde travaille avec une théorie plus ou moins inexacte du programme
- Un ingénieur efficace n’attend pas qu’une personne disposant d’une compréhension parfaite lui donne la réponse ; il prend la décision la mieux fondée possible avec les informations disponibles, puis réagit aux résultats
- Ce travail exige la capacité à prendre position et de la confiance même dans l’incertitude
- Le fait que personne ne connaisse entièrement le comportement global d’un grand produit correspond à la situation décrite dans Nobody knows how software products work
La taille du code à l’époque de Naur et aujourd’hui
- En 1985, lorsque Naur a écrit son article, la taille moyenne des programmes était probablement inférieure de plusieurs ordres de grandeur à celle d’aujourd’hui
- Le premier exemple de grand programme cité par Naur était un programme industriel de surveillance de 200 000 lignes, et le second un compilateur
- La première version de GCC comptait environ 100 000 lignes en 1987, mais dépassait 14 millions de lignes en 2015
- Si les tests existants peuvent être réutilisés, un programme de 100 000 à 200 000 lignes peut être réécrit assez facilement ; il est difficile d’appliquer le même jugement à des systèmes de plus d’un ou deux millions de lignes
Les deux faces des LLM et de la construction de théorie
- Les LLM sont souvent jugés comme de mauvais outils au motif qu’ils nuisent au processus habituel de construction de théorie
- Mais comme d’autres outils logiciels, les LLM ont deux faces
- Ils peuvent rendre plus difficile la création d’un modèle mental détaillé du logiciel
- Ils permettent de construire rapidement une théorie partielle
- Ils peuvent aider à travailler plus efficacement même sur la base d’une compréhension imparfaite
- La relation entre LLM et compréhension du code ne se résume pas à une simple liste d’avantages et d’inconvénients ; c’est un compromis complexe qui exige encore du jugement
Des choix nécessaires même s’ils compliquent la compréhension du code
- Outre les LLM, de nombreux facteurs rendent plus difficile le maintien d’une théorie exacte de la base de code
- Autoriser d’autres personnes à écrire du code dans la même base de code
- Implémenter des fonctionnalités exigées par la loi, comme l’accessibilité ou la protection des données
- Permettre à des collègues de quitter l’entreprise ou de changer d’équipe
- Mettre à niveau des versions logicielles pour appliquer des correctifs de sécurité
- Introduire des bibliothèques ou d’autres dépendances
- On ne peut pas juger qu’un outil ou une pratique est mauvais uniquement parce qu’il entrave la construction de théorie
- La compréhension d’une base de code est l’une des nombreuses valeurs d’ingénierie, au même titre que la lisibilité, la maintenabilité et l’exactitude
- Selon la situation, on peut privilégier une compréhension exacte en sacrifiant d’autres valeurs
- À l’inverse, on peut céder sur la compréhension du code pour des raisons de vitesse, de conformité légale ou de politique interne à l’organisation
- L’objection selon laquelle la compréhension du code serait essentielle pour atteindre toutes les autres valeurs peut s’appliquer de la même manière à la lisibilité, à la maintenabilité et à l’exactitude ; dans la pratique, même ces valeurs fondamentales font constamment l’objet de compromis
Préférences personnelles et responsabilités professionnelles
- Les ingénieurs purs, en particulier, préfèrent travailler en conservant un modèle mental exact
- Le développement est plus amusant et moins stressant
- Cela leur semble plus proche de ce qu’ils considèrent comme la véritable ingénierie
- Si beaucoup d’ingénieurs créent seuls de petits projets open source pendant leur temps libre, c’est aussi parce qu’ils peuvent y maintenir une théorie au sens de Naur exacte de la base de code
- Au travail, il faut suivre l’ensemble de valeurs que l’organisation paie pour obtenir, plutôt que ses valeurs d’ingénierie personnelles
- Même si l’on accorde de l’importance aux performances, on peut écrire du code plus lent pour respecter un calendrier ou accepter des exigences difficiles
- La compréhension complète de la base de code n’est pas non plus un critère absolu à préserver en permanence, mais une option qui peut être échangée contre d’autres valeurs selon les objectifs professionnels
2 commentaires
C’est pour ça qu’on s’accroche autant aux abstractions.
Avis sur Lobste.rs
Le titre de l’article original est malheureusement assez proche du clickbait ; en réalité, le propos est plutôt qu’il faut pouvoir progresser même avec une compréhension partielle d’une grande codebase.
Le point de vue de la construction d’une théorie n’entre pas non plus en conflit avec le cœur de l’article si l’on considère qu’une théorie a à la fois une largeur et une profondeur. La largeur désigne l’étendue du système que l’on comprend suffisamment pour répondre aux questions et y apporter des changements avec aisance ; la profondeur désigne la capacité à répondre à des questions complexes sur une partie donnée et à y effectuer des changements complexes tout en préservant son intégrité.
Si l’on réécrit le code module par module en prévoyant une période de transition, on construit en quelque sorte une théorie étroite mais profonde de ce module, puis on travaille en conséquence. Pour une petite fonction, il peut être acceptable d’ajouter un paramètre ou une valeur de retour, mais à plus grande échelle, il est plus raisonnable de repartir d’une base propre au moins pour un module donné, puis de relier les autres parties afin qu’elles appellent la nouvelle implémentation. Fondamentalement, essayer d’améliorer progressivement la correction d’une implémentation très boguée fonctionne mal, et cela s’applique souvent aussi à des propriétés comme les performances.
Cela dit, lorsque Naur écrivait qu’« au lieu de ranimer un programme, il faut jeter le texte du programme existant et laisser une nouvelle équipe de programmeurs résoudre le problème donné depuis le début », on peut débattre de savoir s’il voulait dire qu’il faut supprimer l’ancien code avant de commencer la réécriture, ou qu’on peut le jeter après avoir vérifié que le nouveau programme constitue un remplacement adéquat.
De plus, dans la réalité, le problème donné lui-même n’est pas donné. Naur suppose que la définition initiale du problème subsiste, mais ce n’est souvent pas le cas dans les grandes codebases. Ce qui s’en rapproche le plus est généralement la suite de tests ; on peut donc remplacer des composants tout en continuant à faire passer les tests.
À l’époque, le déploiement continu n’existait pas non plus. Aujourd’hui, comme le service continue de tourner, qu’il faut assurer les astreintes, répondre aux questions et corriger les bugs urgents, le calcul coût/bénéfice d’une réécriture est différent.
Cette réfutation contient des passages bien formulés, mais la plupart me semblent évidents. Dans un système suffisamment grand, la compréhension partielle est le seul état possible, mais une organisation devrait toujours chercher une compréhension plus profonde.
Indépendamment de l’argument selon lequel « on est payé pour faire le travail », les grands logiciels échouent souvent à satisfaire les utilisateurs ou à atteindre les objectifs business. On peut aussi considérer que Windows et OS X pourrissent sous l’effet de la complexité accumulée.
Les dirigeants feraient disparaître la complexité comme par magie s’ils le pouvaient. Mais il s’agit d’un problème de génie logiciel encore non résolu ; plutôt que de vouloir publier du mauvais code, ils acceptent ce compromis dans les conditions actuelles.
On pourrait se dire que « si l’on a un monopole, cela suffit, donc on peut sortir un logiciel médiocre et plein de bugs », mais c’est trop nihiliste. Il existe beaucoup d’éditeurs qui ne sont pas en situation de monopole, ou dont les utilisateurs ne sont pas captifs ; pour eux, la qualité compte réellement.
Dans cette interprétation, la notion même de compréhension ressemble à une forme de gatekeeping.
L’approche décrite dans l’article est étroitement liée à la capacité de raisonner localement sur une partie du code.
Le raisonnement local, c’est-à-dire comprendre une partie d’une grande codebase sans comprendre tout ce qui l’entoure, est une idée centrale que l’informatique poursuit depuis ses débuts.
L’un des principaux avantages de la programmation structurée était de permettre le raisonnement local, et la recommandation d’éviter les variables globales poursuivait globalement le même objectif. La programmation fonctionnelle rend le raisonnement local possible en éliminant les effets de bord, et l’un des principes de la programmation orientée objet — associer les structures de données au code qui les manipule — fournit lui aussi un autre moyen de raisonner localement.
La véritable force de ces concepts fondamentaux est qu’ils permettent de raisonner et de travailler sur de petites parties du code sans comprendre l’ensemble de la codebase, même si ce n’est pas une situation idéale.
J’ai lu cela comme une réfutation intéressante du conseil selon lequel « il faut comprendre chaque ligne de la codebase », et comme le cœur de l’article touche aux pratiques de développement concrètes, j’ai ajouté le tag
#practices.Je sais que cette communauté n’aime pas les LLM, mais je me demande si cet article relève vraiment de
#vibecoding.vibecoding; je ne suis pas d’accord non plus, mais c’est la pratique. La question centrale de cet article est elle aussi étroitement liée au codage assisté par LLM.Cela dit, je suis d’accord pour dire que
vibecodingne devrait pas être le seul tag. L’article ne traite explicitement des LLM qu’à la toute fin, et son propos principal était déjà intéressant et digne de discussion avant l’ère des LLM. J’estime donc qu’il y a suffisamment de raisons de rétablir le tagpractices, ce que j’ai fait moi-même, et j’encourage les autres à faire de même.Il reste pertinent même dans un monde où l’usage des LLM peut empêcher une compréhension complète de la codebase.
Si le tag signifie « cela peut concerner les personnes intéressées par le vibe coding », alors il est clairement approprié. En revanche, s’il signifie « cela n’a aucun intérêt sauf pour les personnes intéressées par le vibe coding », alors il ne l’est pas. Tous les autres tags sont utilisés dans le premier sens, mais beaucoup d’utilisateurs de Lobsters emploient celui-ci dans le second ; au final, ils ne peuvent donc que passer à côté de cet article.
Une autre caractéristique du développement moderne, différente de 1985, est que même si l’on devient expert d’une codebase de 300 000 lignes, on peut se retrouver la semaine suivante à travailler sur une toute autre codebase de 300 000 lignes.
Les programmeurs expérimentés en activité ont l’habitude de pouvoir être envoyés à tout moment en terrain inconnu, et s’appuient sur une combinaison d’idiomes communs (Google C++ Style Guide), d’outils automatisés comme des compilateurs qui signalent les erreurs en cas de mauvaise utilisation des API, et d’intuitions sur la façon dont les grands programmes « devraient être structurés ».
J’ai aussi vu des discussions du genre « maintenant que plus personne ne comprend le comportement de toute la codebase, comment continuer à développer ? », comme à propos de Bun réécrit avec des LLM, mais cela revient un peu à demander comment on peut comprendre un roman si l’on n’en est pas l’auteur. Au final, c’est du code : il suffit de le lire. Si une fonction n’est pas compréhensible, on peut la découper en plusieurs morceaux, écrire des tests, tracer le flux de contrôle sur papier, ou essayer n’importe quelle autre méthode.
Comme on change régulièrement de projet, il n’y a pas non plus de raison de maintenir sa compréhension des anciennes codebases. À un moment donné, tout finit par se mélanger de façon assez similaire. Même si l’on doit aider le code Scala d’une startup rachetée par l’entreprise à communiquer, via un protocole RPC personnalisé basé sur Thrift, avec un service Ruby JSON en cours de réécriture en Go, une heure de recherche sur
Scala syntax referenceetThrift wire encodingsuffit pour démarrer.Pas besoin non plus de devenir expert. Un mois plus tard, on peut très bien se retrouver à déboguer pourquoi un vérificateur de types basé sur OCaml pour JavaScript plante dans une implémentation en Go du noyau Linux. Au bout du compte, ce n’est que du code.