The Best Idea That Never Caught On

In 1984, Donald Knuth published a paper that proposed a radical inversion. Programs should not be written for compilers and annotated for humans. They should be written as literature for humans, from which compilers extract the executable parts. He called it literate programming, and he built TeX this way.

The idea is beautiful. It is also almost completely absent from modern software development.

TeX remains one of the most reliable and well-understood large programs ever written. That success is partly because Knuth wrote it as a literate program. So the question is not whether literate programming can work. It clearly can. The question is why it worked for Knuth and almost nobody else.

What Literate Programming Actually Means

Knuth’s WEB system, later CWEB for C, works by splitting a single source file into two different outputs. You write a narrative document that explains what the program does, why it does it, and how the pieces fit together. Embedded inside that narrative are fragments of code.

The tangle tool extracts the code fragments, orders them for the compiler, and produces a file the machine can run. The weave tool extracts the prose, formats the code fragments as pretty-printed blocks, and produces a document a human can read.

Here is what a small CWEB program looks like in practice:

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

Notice what is happening. The code is not organized by compilation order. It is organized by narrative logic. The explanation comes first. The implementation follows when the reader is ready for it. The @<Main program@> syntax is a named chunk that can be defined anywhere in the document and assembled by tangle into the correct order for the compiler.

This is the central conceit: the human reads a top-down explanation. The compiler gets a bottom-up dependency graph. Both get the format they prefer.

Why It Worked for Knuth

Knuth is not a typical programmer. He is a mathematician and a writer who happens to write programs. He designs algorithms on paper for months before touching a keyboard. He writes books that are read decades later. When Knuth says code should be literature, he means actual literature, and he has the skill to deliver it.

TeX was also a perfect candidate. It is a batch program with a stable specification. The algorithmic core, especially the line-breaking and page-breaking routines, benefits enormously from extended mathematical explanation. The code changes slowly. The documentation outlives the implementation.

Most software is not like this. Most software changes daily. The specification is discovered, not designed. The audience is not a reader trying to understand an algorithm. It is a developer trying to fix a bug before standup.

The Editing Problem

Literate programming assumes a writing process. Most programming is an editing process.

When you write an essay, you draft, revise, and polish. The structure is planned. When you write software, you explore, test, refactor, and ship. The structure emerges. The documentation in a literate program is not a coating you apply afterward. It is the primary structure. Every time you rename a variable, extract a function, or reorder logic, you are rewriting the literature.

This creates a vicious cycle. If the prose is tightly coupled to the code, refactoring becomes expensive. If the prose is loosely coupled, it drifts from the code and becomes wrong. Either way, the documentation rots.

Modern version control makes this worse. A literate program is a single narrative document. A pull request is a diff against a source tree. Code review tools understand lines in main.c. They do not understand narrative chunks in main.w. The tooling ecosystem, from syntax highlighting to static analysis to CI pipelines, expects compilable source files. Literate programming requires you to opt out of all of it.

The Skill Mismatch

Knuth assumed that programmers are also writers. Most are not.

Good technical writing is rare because it is hard. It requires empathy for a reader who does not yet understand what you understand. It requires the discipline to explain why, not just what. It requires editing, which is a separate skill from coding.

When you ask a team to write literate programs, you are asking them to be Knuth. You are asking for extended prose that explains intent, explores alternatives, and guides a reader through the reasoning. The median code comment is // TODO: fix this. The gap between that and literature is not a tooling problem. It is a human problem.

This is why attempts to revive literate programming with better tools have consistently failed. The bottleneck was never the weave and tangle toolchain. The bottleneck is that most programmers do not want to write essays, and most codebases do not reward essay-quality documentation.

What Actually Replaced It

The industry did not abandon Knuth’s goal. It achieved a weaker version of it through different means.

Type systems now encode intent that used to require paragraphs of explanation. When a function accepts NonEmptyList<T> instead of List<T>, the type checker enforces a guarantee that documentation could only describe. When Rust’s borrow checker rejects a reference escape, it communicates a constraint that would take pages to explain in prose.

Docstrings and API documentation tools like Javadoc, Rustdoc, and TypeDoc created a middle ground. They keep documentation adjacent to code without requiring it to drive the structure. You can read the source or the generated docs, and both stay in sync because they live in the same file.

Test suites became executable documentation. A well-written test says “given this input, expect this output” more precisely than prose ever could. Property-based tests encode invariants. Snapshot tests capture intent. The tests run on every commit, so they cannot drift the way prose does.

Perhaps most importantly, programming languages themselves became more literate. Python reads like pseudocode. Rust’s if let syntax expresses pattern matching intent directly. When the code is the explanation, you do not need a separate explanation of the code.

The Modern Version That Did Work

There is one place where Knuth’s vision survived nearly intact: computational notebooks.

Jupyter notebooks, R Markdown, and Observable allow prose and code to coexist in a single document. The prose explains. The code executes. The output appears inline. This is literate programming in everything but name.

Notebooks succeeded where WEB failed because the context is different. A data scientist exploring a dataset is engaged in a narrative process. The code changes slowly. The audience is a human reader trying to understand a decision. The editing cycle is exploration, not refactoring. The constraints that killed literate programming in software engineering are features in data science.

The lesson is not that literate programming was wrong. It is that the idea has a narrow ecological niche. It thrives when the program is a finished artifact meant to be studied. It suffocates when the program is a living system meant to be changed.

What to Take From It

You are probably not going to adopt CWEB for your next service. That is fine. But Knuth’s underlying insight is still worth applying.

Name things so they explain themselves. A function called process_data is a failure of literate programming even if you never touch WEB. A function called remove_expired_sessions_older_than carries its own documentation.

Write the explanation you wish you had. If a section of code requires a paragraph of prose to understand, write the paragraph. Put it in a comment, a docstring, or a design doc. The medium is less important than the act of explaining.

Separate the narrative structure from the compiler’s structure when it helps. README-driven development, architecture decision records, and RFCs are all ways to write the human-facing story without fighting your toolchain.

Knuth wanted us to write programs like books. We ended up writing books about programs instead. That is a weaker victory than he hoped for, but it is still a victory.