Die beste Idee, die nie durchgesetzt hat

1984 veröffentlichte Donald Knuth einen Aufsatz, der eine radikale Umkehrung vorschlug. Programme sollten nicht für Compiler geschrieben und für Menschen kommentiert werden. Sie sollten als Literatur für Menschen verfasst werden, aus der Compiler die ausführbaren Teile extrahieren. Er nannte es literate programming und baute TeX auf diese Weise.

Die Idee ist wunderschön. Sie ist auch in der modernen Softwareentwicklung so gut wie nicht vorzufinden.

TeX bleibt eines der zuverlässigsten und am besten verstandenen großen Programme, die je geschrieben wurden. Dieser Erfolg ist teilweise darauf zurückzuführen, dass Knuth es als literate program verfasste. Die Frage ist also nicht, ob literate programming funktionieren kann. Das tut es offensichtlich. Die Frage ist, warum es bei Knuth funktionierte und bei fast niemandem sonst.

Was Literate Programming tatsächlich bedeutet

Knuths WEB-System, später CWEB für C, arbeitet, indem es eine einzelne Quelldatei in zwei verschiedene Ausgaben aufteilt. Man verfasst ein erzählendes Dokument, das erklärt, was das Programm tut, warum es das tut und wie die Teile zusammenpassen. In diese Erzählung eingebettet sind Code-Fragmente.

Das Tool tangle extrahiert die Code-Fragmente, ordnet sie für den Compiler und erzeugt eine Datei, die die Maschine ausführen kann. Das Tool weave extrahiert die Prosa, formatiert die Code-Fragmente als hübsch gedruckte Blöcke und erzeugt ein Dokument, das ein Mensch lesen kann.

So sieht ein kleines CWEB-Programm in der Praxis aus:

@* 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;
}

Beachten Sie, was hier passiert. Der Code ist nicht nach Kompilationsreihenfolge organisiert. Er ist nach narrativer Logik organisiert. Die Erklärung kommt zuerst. Die Implementierung folgt, wenn der Leser dafür bereit ist. Die Syntax @<Main program@> ist ein benannter Block, der an beliebiger Stelle im Dokument definiert und von tangle in die korrekte Reihenfolge für den Compiler zusammengesetzt werden kann.

Das ist der Kern der Sache: Der Mensch liest eine top-down-Erklärung. Der Compiler erhält einen bottom-up dependency graph. Beide erhalten das Format, das sie bevorzugen.

Warum es bei Knuth funktionierte

Knuth ist kein typischer Programmierer. Er ist ein Mathematiker und Schriftsteller, der zufällig Programme schreibt. Er entwirft Algorithmen monatelang auf Papier, bevor er eine Tastatur berührt. Er schreibt Bücher, die Jahrzehnte später gelesen werden. Wenn Knuth sagt, Code solle Literatur sein, meint er tatsächliche Literatur, und er hat das Können, dies zu liefern.

TeX war auch ein perfekter Kandidat. Es ist ein Batch-Programm mit einer stabilen Spezifikation. Der algorithmische Kern, insbesondere die Zeilenumbruch- und Seitenumbruch-Routinen, profitiert enorm von ausgedehnten mathematischen Erklärungen. Der Code ändert sich langsam. Die Dokumentation überdauert die Implementierung.

Die meiste Software ist nicht so. Die meiste Software ändert sich täglich. Die Spezifikation wird entdeckt, nicht entworfen. Das Publikum ist nicht ein Leser, der einen Algorithmus verstehen will. Es ist ein Entwickler, der versucht, einen Bug vor dem Stand-up zu beheben.

Das Bearbeitungsproblem

Literate programming geht von einem Schreibprozess aus. Die meiste Programmierung ist ein Bearbeitungsprozess.

Wenn man einen Aufsatz schreibt, entwirft man, überarbeitet und poliert. Die Struktur ist geplant. Wenn man Software schreibt, erforscht man, testet, refactort und liefert aus. Die Struktur entsteht. Die Dokumentation in einem literate program ist kein Überzug, den man nachträglich aufträgt. Sie ist die primäre Struktur. Jedes Mal, wenn man eine Variable umbenennt, eine Funktion extrahiert oder die Logik neu ordnet, schreibt man die Literatur um.

Das schafft einen Teufelskreis. Wenn die Prosa eng an den Code gekoppelt ist, wird Refactoring teuer. Wenn die Prosa lose gekoppelt ist, driftet sie vom Code ab und wird falsch. So oder so verrottet die Dokumentation.

Moderne Versionskontrolle verschärft das noch. Ein literate program ist ein einziges erzählendes Dokument. Ein Pull Request ist ein Diff gegen einen Quellcode-Baum. Code-Review-Tools verstehen Zeilen in main.c. Sie verstehen keine narrativen Blöcke in main.w. Das Tooling-Ökosystem, von Syntax-Highlighting über statische Analyse bis hin zu CI-Pipelines, erwartet kompilierbare Quelldateien. Literate programming erfordert, dass man sich aus allem davon ausschließt.

Das Kompetenzmismatch

Knuth ging davon aus, dass Programmierer auch Schriftsteller sind. Die meisten sind es nicht.

Gutes technisches Schreiben ist selten, weil es schwer ist. Es erfordert Einfühlungsvermögen für einen Leser, der noch nicht versteht, was man selbst versteht. Es erfordert die Disziplin zu erklären, warum, nicht nur was. Es erfordert Redaktion, was eine separate Fähigkeit vom Programmieren ist.

Wenn man ein Team bittet, literate programs zu schreiben, bittet man sie, Knuth zu sein. Man bittet um ausgedehnte Prosa, die Absicht erklärt, Alternativen erforscht und einen Leser durch die Argumentation führt. Der mittlere Code-Kommentar ist // TODO: fix this. Die Kluft zwischen dem und Literatur ist kein Tooling-Problem. Es ist ein menschliches Problem.

Das ist der Grund, warum Versuche, literate programming mit besseren Tools wiederzubeleben, konsequent gescheitert sind. Der Engpass war nie die weave- und tangle-Toolchain. Der Engpass ist, dass die meisten Programmierer keine Aufsätze schreiben wollen, und die meisten Codebases keine Aufsatz-Qualität-Dokumentation belohnen.

Was es tatsächlich ersetzt hat

Die Industrie hat Knuths Ziel nicht aufgegeben. Sie hat eine schwächere Version davon auf andere Weise erreicht.

Typsysteme kodieren jetzt Absicht, die früher Absätze von Erklärungen erforderte. Wenn eine Funktion NonEmptyList<T> statt List<T> akzeptiert, erzwingt der Type Checker eine Garantie, die Dokumentation nur beschreiben konnte. Wenn Rusts borrow checker eine Referenz-Eskape ablehnt, kommuniziert er eine Einschränkung, die man in Prosa Seiten bräuchte, um zu erklären.

Docstrings und API-Dokumentations-Tools wie Javadoc, Rustdoc und TypeDoc schufen einen Mittelweg. Sie halten Dokumentation nah am Code, ohne zu verlangen, dass sie die Struktur bestimmt. Man kann den Quellcode oder die generierte Doku lesen, und beide bleiben synchron, weil sie in derselben Datei leben.

Test-Suiten wurden zu ausführbarer Dokumentation. Ein gut geschriebener Test sagt “gegeben diese Eingabe, erwarte diese Ausgabe” präziser, als Prosa es je könnte. Property-based Tests kodieren Invarianten. Snapshot Tests erfassen Absicht. Die Tests laufen bei jedem Commit, also können sie nicht so driftieren wie Prosa.

Vielleicht am wichtigsten wurden Programmiersprachen selbst literater. Python liest sich wie Pseudocode. Rusts if let-Syntax drückt Pattern-Matching-Absicht direkt aus. Wenn der Code die Erklärung ist, braucht man keine separate Erklärung des Codes.

Die moderne Version, die funktionierte

Es gibt einen Ort, an dem Knuths Vision fast unverändert überlebt hat: Computational Notebooks.

Jupyter Notebooks, R Markdown und Observable erlauben es Prosa und Code, in einem einzigen Dokument zu koexistieren. Die Prosa erklärt. Der Code führt aus. Die Ausgabe erscheint inline. Das ist literate programming in allem außer dem Namen.

Notebooks hatten Erfolg, wo WEB scheiterte, weil der Kontext anders ist. Ein Data Scientist, der einen Datensatz erforscht, ist in einem narrativen Prozess engagiert. Der Code ändert sich langsam. Das Publikum ist ein menschlicher Leser, der versucht, eine Entscheidung zu verstehen. Der Bearbeitungszyklus ist Exploration, nicht Refactoring. Die Einschränkungen, die literate programming in der Software-Entwicklung getötet haben, sind in der Data Science Features.

Die Lehre ist nicht, dass literate programming falsch war. Es ist, dass die Idee eine ökologische Nische hat. Sie gedeiht, wenn das Programm ein fertiges Artefakt ist, das studiert werden soll. Sie erstickt, wenn das Programm ein lebendes System ist, das geändert werden soll.

Was man daraus mitnehmen sollte

Sie werden wahrscheinlich nicht CWEB für Ihren nächsten Service einführen. Das ist in Ordnung. Aber Knuths zugrundeliegender Einblick ist es immer noch wert, angewendet zu werden.

Benennen Sie Dinge so, dass sie sich selbst erklären. Eine Funktion namens process_data ist ein Scheitern von literate programming, auch wenn Sie WEB nie berühren. Eine Funktion namens remove_expired_sessions_older_than trägt ihre eigene Dokumentation mit sich.

Schreiben Sie die Erklärung, die Sie gerne gehabt hätten. Wenn ein Abschnitt Code einen Absatz Prosa erfordert, um verstanden zu werden, schreiben Sie den Absatz. Packen Sie ihn in einen Kommentar, einen Docstring oder ein Design-Dokument. Das Medium ist weniger wichtig als der Akt des Erklärens.

Trennen Sie die narrative Struktur von der Struktur des Compilers, wenn es hilft. README-driven Development, Architecture Decision Records und RFCs sind allesamt Wege, die menschenorientierte Geschichte zu schreiben, ohne gegen die Toolchain zu kämpfen.

Knuth wollte, dass wir Programme wie Bücher schreiben. Wir endeten damit, Bücher über Programme zu schreiben. Das ist ein schwächerer Sieg, als er gehofft hatte, aber es ist immer noch ein Sieg.