Konubinix' opinionated web of thoughts

How to Do Literate Programming

Braindump

Prose is the primary artifact

A literate program is first and foremost a piece of writing. The prose carries the full reasoning — problem, solution, and motivation. Code blocks are inline translations of specific sentences in the prose; they render the technical form of what the prose already stated.

Prose is richer than code: the why is never translated, and a code comment is not where it lives. A comment carrying a why — a system state, a design constraint, a justification of structure — is prose stranded in the wrong file: invisible to the narrative reader, unmoored for the code reader. The reasoning belongs in the paragraph that motivates the block. What remains in code is at most a one-liner annotating the line below.

Sanity check: mentally remove all code blocks. The remaining text must form a coherent and complete narrative. If a gap appears, either a paragraph is missing, or an existing paragraph was only paraphrasing a block rather than advancing the reasoning — and needs to be rewritten accordingly. This is the weaving: code and prose are interleaved, not inserted side-by-side. Tests give a binary signal, but prose coherence doesn’t — a neutral agent reading the prose-only version and assessing whether the narrative fulfills the stated need provides a stronger signal than code-level tests alone.

The reading flow is sacred

The reader should glide through the prose uninterrupted, glancing at a code block whenever they want to see how a sentence is concretely coded. Everything else — detours, meta-commentary, secondary justifications — is noise and must be removed. If something is valuable but breaks the flow — a technical derivation, a self-contained construction whose result the narrative will consume — move it to an annex and link to it from the main narrative at the moment its result is used.

Put a lot of links. If the reader wants to follow a concept referred to and described somewhere else, per must be able to simply click and go there.

A block that cannot be taken in at a glance breaks the flow just as surely as a detour. Around 30 lines is a reasonable ceiling; past that, the reader loses the thread between the sentence that motivated the block and the code that is supposed to translate it. The fix is almost always to split the block along the seams the prose already has — each responsibility that was introduced as a distinct idea becomes its own block, with a sentence of motivation just before. The noweb composition reassembles the pieces at tangle time. A block that resists splitting signals that the prose itself has collapsed several ideas into one — expand the prose first, and the block will split naturally.

Not every short conversational line is noise, though. A single sentence like “let’s see what this gives” just before a block that produces something visible — an image, an exported model, a plot — synchronizes the prose with the reader’s impatience to leave abstraction and see a concrete result. It does not interrupt the flow; it paces it. The test is whether the line names something the reader is already feeling: if it does, keep it. If it instead introduces new content, it is no longer a pacing line but a regular paragraph in disguise — expand it or cut it.

Code blocks are not repeated in the export

A direct consequence of the reading flow principle: a code block appears in the export only when the prose talks about it. Re-showing a block that the reader has already seen, just because the next block depends on it, adds nothing to the reading and becomes noise. The dependency is resolved at tangle time, invisibly to the reader. Repetition is only justified when seeing the surrounding code genuinely helps the reading.

Prose states the what, the how, and the why

Every code block is motivated by what precedes it in the prose:

  • The what (the desired behavior) → translated into a test
  • The how (the chosen technical approach) → translated into code
  • The why (the reason for the choice) → stays in prose, untranslated

No block should exist unless the prose has made it necessary. If you find yourself adding a block without the prose having led to it, a piece of reasoning is missing. The inverse also holds: prose that paraphrases the block — restating what the code does — is duplication, not justification. Prose states the problem or need, not a readback of the code. The exception is the translation aside (see below): when the gap between intent and form is wide enough that a short readback is what bridges it, the readback earns its place.

The rule applies to the reading — not to the source. A document’s tangle scaffolding lives invisibly, usually in :noexport: sections or marked :exports none. These blocks need no motivating prose because they are not read; they exist only to make tangling work.

How prose and code are sliced against each other is arbitrary: one block per symbol, several symbols per block, several blocks per symbol — all are legitimate. The only criterion is the reading flow. If the reader pauses on a function and asks “wait, what is this for? I was reading about something abstract and I don’t see the connection,” the slicing has failed. Boilerplate — every getter and setter of a Java bean, say — does not earn a sentence each; that would drown the reader in motivation no one needed. Conversely, a block dropping several unrelated functions under a single vague paragraph leaves the reader unmoored, and calls either for more sentences or for splitting the block. The fix is whichever restores the flow.

Vocabulary follows the same rule. The states a field can take, the thresholds that govern behavior, the constants that pace the system are named in prose, not in a type comment listing valid values, a magic number setting a tempo, or a literal fixing a bound.

The ordering falls out of this split: the test (the what) comes before the code (the how), because that is how we talk to each other — we state the desired behavior first, then the technique used to achieve it. A test that stands apart as a separate paragraph, not flowing from an explicit assertion in the prose, is a sign that a sentence is missing from the narrative.

Tests can be fused for that allows factoring the fixtures and make them run faster. But they must be fused only if we can prove that one does not impact the other. Also, the way they appear in the document MUST NOT be fused. One test is one what. A test block asserting several whats breaks the prose flow.

Prose: "a duplicate in the list would make the tally incoherent,
        so we block creation and display an error message"

  [test — translates "we block and display a message"]
  [code — translates "we validate the list and expose a createError getter"]

Translation asides

Sometimes the translation is not obvious: the intent is clear but its shape in code is surprising. In that case a short paragraph can act as a translator’s note: “we want X, but in code this is written as Y because Z”.

This is not code description — it is an explanation of the gap between intent and form. These asides should be rare and clearly identifiable as such (e.g. introduced by “In practice,” or “Under the hood,”).

When an aside grows beyond its role — explaining more than the translation gap — it is better moved to an annex to avoid breaking the reading flow. The prose then points to it with a short link, e.g. “in order to understand X, see annex” (with a link).

Feeling the need for an aside is also a diagnostic. Sometimes it reflects a genuine gap between what human language and code can express — that is what the aside is for. But it can also reveal that the code itself is poorly organized: the surprising shape that needs a translator’s note may simply be the wrong shape. The structure of the code is not bound to the structure of the prose — noweb reassembles the pieces at tangle time, so the code can be carved along its own joints. A poorly written block is never excused by the fact that the prose happened to flow in another direction; in that case, refactor the code rather than paper over it with an aside.

Prose: "we want the getters to behave as real properties"

  Under the hood, this means using defineProperty rather than Object.assign,
  which does not copy accessors.

  [code — translates this technical constraint]

Rewrite, don’t patch

When something forces the document to change — a bug, an oversight, a change of mind, a breaking change in an upstream library, a refactor that obsoletes an earlier choice — the temptation is to add a test and an explanatory paragraph describing the mitigation. This does not restore the narrative — it leaves a visible scar. Instead, rewrite the affected prose section so it reads as if the issue never existed: not a narrative that fixes something, but a narrative that was always correct. The reading flow is sacred, and a scar interrupts it just as surely as a detour does — the final artifact should betray no trace of the path that led to it.

The scar is retrospective — a trace of a revision already applied, a debate already settled. Prospective language is different: honest framing of a value still to validate empirically, of an explicit test plan, or of a guard-rail against a tempting wrong path. These are not scars. The distinguishing test is tense: does the sentence describe a fix already made (scar — rewrite), or an open question the reader should know about (caveat — keep)?

Scar — rewrite:
  "initially at 120 mm/s, since revised to 60 mm/s for volumetric flow"

Caveat — keep:
  "clearance at 0.5 mm — to adjust by trial if the real fit is too loose"

Code carries scars too. When reasoning leaves the code — a justification moved to prose, a constraint absorbed into a cleaner design — the code that only carried it leaves as well. An empty body, a one-armed conditional, a vestigial helper: each is a fragment whose work now lives elsewhere. The artifact reads as if neither prose nor code remembered the revision.

Suggested prompt

To audit a document against these principles, build a table. Each row is one observation: the file and line it comes from, a short description of what was seen, and the principle it concerns. The conclusion of the audit is whatever the table shows once it is complete — it is not written in advance and then defended. A verdict that does not point back to specific lines is a guess; if you cannot fill the table, you have not read the document closely enough to judge it.