La meilleure idée qui n’a jamais pris
En 1984, Donald Knuth a publié un article proposant une inversion radicale. Les programmes ne devraient pas être écrits pour les compilateurs et annotés pour les humains. Ils devraient être écrits comme de la littérature pour les humains, dont les compilateurs extraient les parties exécutables. Il a appelé cela literate programming, et il a construit TeX de cette façon.
L’idée est belle. Elle est aussi presque complètement absente du développement logiciel moderne.
TeX reste l’un des programmes les plus fiables et les mieux compris jamais écrits. Ce succès est en partie dû au fait que Knuth l’a écrit comme un literate program. La question n’est donc pas de savoir si le literate programming peut fonctionner. Il est clair que oui. La question est de savoir pourquoi cela a fonctionné pour Knuth et pour presque personne d’autre.
Ce que signifie réellement la programmation lettrée
Le système WEB de Knuth, puis CWEB pour le C, fonctionne en divisant un seul fichier source en deux sorties différentes. Vous écrivez un document narratif qui explique ce que fait le programme, pourquoi il le fait et comment les pièces s’assemblent. Des fragments de code sont intégrés dans cette narration.
L’outil tangle extrait les fragments de code, les ordonne pour le compilateur et produit un fichier que la machine peut exécuter. L’outil weave extrait la prose, formate les fragments de code sous forme de blocs bien présentés et produit un document qu’un humain peut lire.
Voici à quoi ressemble un petit programme CWEB dans la pratique :
@* Introduction. This program computes the sum of a list of integers.
The algorithm is straightforward: iterate through the array and accumulate.
@c
#include <stdio.h>
@<Function prototypes@>;
@<Main program@>;
@*2 The core logic.
We define |sum_array| to take a pointer and a length, returning the total.
@<Function definitions@>=
int sum_array(const int *arr, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) {
total += arr[i];
}
return total;
}
@*2 Entry point.
A small driver to exercise the function.
@<Main program@>=
int main(void) {
int data[] = {1, 2, 3, 4, 5};
size_t len = sizeof(data) / sizeof(data[0]);
printf("%d\n", sum_array(data, len));
return 0;
}
Remarquez ce qui se passe. Le code n’est pas organisé par ordre de compilation. Il est organisé par logique narrative. L’explication vient en premier. L’implémentation suit lorsque le lecteur est prêt. La syntaxe @<Main program@> est un fragment nommé qui peut être défini n’importe où dans le document et assemblé par tangle dans l’ordre correct pour le compilateur.
C’est l’idée centrale : l’humain lit une explication de haut en bas. Le compilateur obtient un graphe de dépendances de bas en haut. Chacun obtient le format qu’il préfère.
Pourquoi cela a fonctionné pour Knuth
Knuth n’est pas un programmeur typique. C’est un mathématicien et un écrivain qui écrit des programmes par hasard. Il conçoit des algorithmes sur papier pendant des mois avant de toucher un clavier. Il écrit des livres qui sont lus des décennies plus tard. Quand Knuth dit que le code devrait être de la littérature, il veut dire de la vraie littérature, et il a le talent pour y parvenir.
TeX était également un candidat parfait. C’est un programme batch avec une spécification stable. Le cœur algorithmique, en particulier les routines de césure et de saut de page, bénéficie énormément d’explications mathématiques étendues. Le code change lentement. La documentation survit à l’implémentation.
La plupart des logiciels ne sont pas comme ça. La plupart des logiciels changent quotidiennement. La spécification est découverte, pas conçue. L’audience n’est pas un lecteur qui essaie de comprendre un algorithme. C’est un développeur qui essaie de corriger un bug avant la réunion quotidienne.
Le problème de l’édition
La programmation lettrée suppose un processus d’écriture. La plupart de la programmation est un processus d’édition.
Quand vous écrivez un essai, vous rédigez, révisez et peaufinez. La structure est planifiée. Quand vous écrivez un logiciel, vous explorez, testez, refactorisez et livrez. La structure émerge. La documentation dans un literate program n’est pas un revêtement que vous appliquez après coup. C’est la structure principale. Chaque fois que vous renommez une variable, extrayez une fonction ou réordonnez la logique, vous réécrivez la littérature.
Cela crée un cycle vicieux. Si la prose est étroitement couplée au code, le refactoring devient coûteux. Si la prose est faiblement couplée, elle dérive du code et devient fausse. Dans les deux cas, la documentation pourrit.
Le contrôle de version moderne aggrave la situation. Un literate program est un document narratif unique. Une pull request est un diff contre un arbre de sources. Les outils de revue de code comprennent les lignes dans main.c. Ils ne comprennent pas les fragments narratifs dans main.w. L’écosystème d’outils, du surlignage de syntaxe à l’analyse statique en passant par les pipelines CI, attend des fichiers source compilables. La programmation lettrée vous oblige à vous exclure de tout cela.
Le décalage de compétences
Knuth supposait que les programmeurs sont aussi des écrivains. La plupart ne le sont pas.
La bonne écriture technique est rare parce qu’elle est difficile. Elle exige de l’empathie pour un lecteur qui ne comprend pas encore ce que vous comprenez. Elle exige la discipline d’expliquer pourquoi, pas seulement quoi. Elle exige de l’édition, qui est une compétence distincte de la programmation.
Quand vous demandez à une équipe d’écrire des literate programs, vous lui demandez d’être Knuth. Vous demandez une prose étendue qui explique l’intention, explore les alternatives et guide un lecteur à travers le raisonnement. Le commentaire de code moyen est // TODO: fix this. L’écart entre cela et la littérature n’est pas un problème d’outils. C’est un problème humain.
C’est pourquoi les tentatives de faire revivre la programmation lettrée avec de meilleurs outils ont systématiquement échoué. Le goulot d’étranglement n’a jamais été la toolchain weave et tangle. Le goulot d’étranglement est que la plupart des programmeurs ne veulent pas écrire d’essais, et la plupart des bases de code ne récompensent pas une documentation de qualité littéraire.
Ce qui l’a réellement remplacé
L’industrie n’a pas abandonné l’objectif de Knuth. Elle a atteint une version plus faible de celui-ci par des moyens différents.
Les systèmes de types encodent désormais une intention qui nécessitait auparavant des paragraphes d’explication. Quand une fonction accepte NonEmptyList<T> au lieu de List<T>, le type checker applique une garantie que la documentation ne pouvait que décrire. Quand le borrow checker de Rust rejette une échappée de référence, il communique une contrainte qu’il faudrait des pages pour expliquer en prose.
Les docstrings et les outils de documentation d’API comme Javadoc, Rustdoc et TypeDoc ont créé un juste milieu. Ils maintiennent la documentation adjacente au code sans exiger qu’elle dicte la structure. Vous pouvez lire le code source ou la documentation générée, et les deux restent synchronisés parce qu’ils vivent dans le même fichier.
Les suites de tests sont devenues une documentation exécutable. Un test bien écrit dit “étant donné cette entrée, attendez cette sortie” plus précisément que la prose ne le pourrait jamais. Les tests property-based encodent des invariants. Les tests snapshot capturent l’intention. Les tests s’exécutent à chaque commit, donc ils ne peuvent pas dériver comme le fait la prose.
Peut-être plus important encore, les langages de programmation eux-mêmes sont devenus plus lettrés. Python se lit comme du pseudocode. La syntaxe if let de Rust exprime directement l’intention du pattern matching. Quand le code est l’explication, vous n’avez pas besoin d’une explication séparée du code.
La version moderne qui a fonctionné
Il existe un endroit où la vision de Knuth a survécu presque intacte : les computational notebooks.
Jupyter notebooks, R Markdown et Observable permettent à la prose et au code de coexister dans un seul document. La prose explique. Le code s’exécute. La sortie apparaît inline. C’est du literate programming en tout sauf en nom.
Les notebooks ont réussi là où WEB a échoué parce que le contexte est différent. Un data scientist explorant un jeu de données est engagé dans un processus narratif. Le code change lentement. L’audience est un lecteur humain qui essaie de comprendre une décision. Le cycle d’édition est l’exploration, pas le refactoring. Les contraintes qui ont tué la programmation lettrée en ingénierie logicielle sont des atouts en science des données.
La leçon n’est pas que la programmation lettrée avait tort. C’est que l’idée a une niche écologique étroite. Elle prospère quand le programme est un artefact fini destiné à être étudié. Elle étouffe quand le programme est un système vivant destiné à être changé.
Quoi en retirer
Vous n’allez probablement pas adopter CWEB pour votre prochain service. C’est normal. Mais l’idée sous-jacente de Knuth vaut toujours la peine d’être appliquée.
Nommez les choses pour qu’elles s’expliquent d’elles-mêmes. Une fonction appelée process_data est un échec de la programmation lettrée même si vous ne touchez jamais à WEB. Une fonction appelée remove_expired_sessions_older_than porte sa propre documentation.
Écrivez l’explication que vous auriez aimé avoir. Si une section de code nécessite un paragraphe de prose pour être comprise, écrivez le paragraphe. Mettez-le dans un commentaire, un docstring ou un document de conception. Le médium est moins important que l’acte d’expliquer.
Séparez la structure narrative de la structure du compilateur quand cela aide. Le développement piloté par README, les architecture decision records et les RFCs sont autant de façons d’écrire l’histoire orientée vers l’humain sans se battre contre votre toolchain.
Knuth voulait que nous écrivions des programmes comme des livres. Nous avons fini par écrire des livres sur les programmes. C’est une victoire plus faible qu’il ne l’espérait, mais c’est quand même une victoire.