Literate Programming
Table of Contents
1. Overview
Don Knuth coined the term "Literate Programming" to describing a method of exposition coupled to writing computer software. It was both literature and software. Unfortunately, since Javadoc, everyone has co-opted the term to refer to inline documentation of software, or something along those lines.
Knuth's idea works well for static projects, or for writing code which non-programmer experts review. The latter case takes advantage of the "Web style" the documentation affords, so a non-programmer can read it as if it were pseudocode.
But it doesn't adequately work with agile methods. I'm not even clear how to unit test code in this manner. If I had to iteratively refine some code, I wouldn't know how to do that effectively. There have been times when I wanted to create a stub, then revise it later (so the chapter could form a self-contained program). Literate programming doesn't handle the "then revise it later" step. Presumably I could have used function pointers to abstract away the methods used, or some other fancy trick.
2. Knuth's Web and CWeb
The original software Knuth used for literate programming which allowed the author to extract Pascal code, which was then compiled into a binary for execution. This is how Knuth wrote TeX, which was then used to write Web (to make the documentation pretty).
A web program consists of "chunks". Each chunk was numbered, and
had some optional English text, followed by an optional source code
fragment, but must have at least one of either. (Empty chunks were
not allowed.) The code snippet could refer to other chunks as a
sort of pseudocode, writing something like <Sort applicants by
height>
then later implement the code in a chunk named "Sort
applicants by height".
But Pascal didn't have the longevity of C. Thus begat CWeb. It had the same style of chunks, labels, etc., but used C code instead of Pascal.
2.1. Intention: Communicate with Human Beings
Knuth explicitly pleas in his article "Literate Programming" (1984):
Let us change the traditional attitude to the construction of programs. Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.
The practitioner of literate programming can be regarded as an essayist, whose main concern is with exposition and excellence of style. Such an author, with thesaurus in hand, chooses the names of variables carefully and explains what each variable means. He or she strives for a program that is comprehensible because its concepts have been introduced in an order that is best for human understanding, using a mixture of formal and informal methods that nicely reinforce each other.
Sadly programmers do not view themselves as essayists while programming. It's probably for this reason that literate programming never caught on.
2.1.1. Discussion of Variables
Curiously, Knuth reflects about a few stylistic issues:
The name of a section, enclosed in angle brackets, should be long enough to encapsulate the essential characteristics of the code in that section, but it should not be too verbose. I found very early that it would be a mistake to include all of the assumptions about local and global variables in the name of each section, even though such information would strictly be necessary to isolate that section as an independent module. The trick is to find a balance between formal and informal exposition so that a reader can grasp what is happening without being overwhelmed with detail. (See Naur [14].)
I completely misread this the first time I read the article as discouraging the discussion of variables in the body of a chunk.
2.1.2. "Psychologically correct order"
Knuth refers to the strength of literate programming as not being forced to pick between "top-down" versus "bottom-up" design, stating in his article "Literate Programming":
And the fact that there's no need to be hung up on the question of top-down versus bottom-up—since a programmer can now view a large program as a web, to be explored in a psychologically correct order—is perhaps the greatest lesson I have learned from my recent experiences.
I have been confused by this enigmatic phrase for a while. But when
reading his article on structured programming with the goto
statement,
Knuth writes:
Putting this another way, it is much better from a psychological standpoint to write
loop until found ... ; found; ... repeatthan to write
search: while true do begin ... ; leave search; ... end.The
leave
orexit
statement is operationally the same, but intuitively different, since it talks more about the program than about the problem.
(Bold emphasis mine.)
It seems that one aspect of Knuth's use of the term "psychological" is working at the level of abstraction suitable for stating the problem, in a manner which is readily understandable and conceptually simple.
2.2. Style
Chunk names seem to be frequently in the imperative mood ("See foo
",
"Initialize bar
", "Fetch [data]
", etc.), at least in the TeXbook.
One observation is that Knuth usually writes in Pascal a function's
parameters and local variables, then uses "pseudocode chunk references"
like <Initialize bar>
in the function's body.
Some numbers:
- 1379 chunks in
tex.web
- 6115 lines in
tex.p
(the tangled code), for an average of 4.434 lines of Pascal code per chunk - 1216
@d
definitions intex.web
in 214 chunks - 13
@f
format specifiers intex.web
- 125 chunks have no code segment
- 45 chunks have no code segment and no definition/formats
- 1254 chunks have code
- 504 chunks have no commentary (all of them have code, none have definitions or formats)
- Among all chunks,
- average of 11.066 lines of code
- median 9 lines of code
- 25th quartile: 4 lines of code
- 75h quartiles: 16
- standard deviation: 9.14 lines of code
- maximum of 115 lines of code in a single chunk
- The quartiles among all chunks of code: 0 and 13 lines of code
- Among the chunks with code,
- average of 12.16905 lines of code
- standard deviation of 8.858455 lines of code
- median of 10 lines of code
- minimum of 2 lines of code
- 25h quartile: 6 lines of code
- 75th quartile: 16 lines of code
2.2.1. Naming Sections
Knuth has a number of "lessons learned" in the section "Stylistic Issues" of his article on literate programming:
The name of a section, enclosed in angle brackets, should be long enough to encapsulate the essential characteristics of the code in that section, but it should not be too verbose. I found very early that it would be a mistake to include all of the assumptions about local and global variables in the name of each section, even though such information would strictly be necessary to isolate that section as an independent module. The trick is to find a balance between formal and informal exposition so that a reader can grasp what is happening without being overwhelmed with detail. (See Naur [14].)
Another lesson I learned early in the game was that the name of a section should explicitly mention any nonstandard control structures, even though its data structures can often be left implied. Furthermore, if the control flow is properly explained, you can avoid the usual errors associated with
goto
statements; such statements can safely be introduced in a restrained but natural manner.
3. Misinterpretations of Knuth
Following Knuth's enthusiastic examples, Javadoc added documentation for Java code using comments. (Lisp had this earlier in comment strings, but Javadoc extracted documentation from specific comments.) Since then, the programming community had misinterpreted "literate programming" as "programming with API documentation embedded as comments".
Jupyter (formerly Python Notebooks) tried to introduce something closer to Knuth's sense of literate programming, but it didn't really adhere to Knuth's intentions. Org-babel seems to work along similar lines as Jupyter.
4. My own experiences
For the most part, it's fine. After having acquired a taste for unit testing, it's a little unnatural at first (since C doesn't have a unit testing framework). My exposition has been at the level of documenting design decision, discussing alternatives, and so on. I suspect this is not quite what Knuth had in mind.
The best example of a literate program, to my mind, is Bob Nystrom's Crafting Interpreters. He wrote his own toolkit to splice into his book (written in markdown) segments of code from a C program (or Java program). This probably tells a lot about the approach taken to writing a literate program, that the best one wrote the code first (albeit with explanations in mind) and worked a book around the code.
I dislike org-mode's literate programming features, since it doesn't mesh well with org-mode. There's no way to create "chunks" the same way I could in cweb. On the other hand, each chunk is really six or seven stars, and I get to cluster them together in a way that is natural for org-mode.
4.1. Thoughts on Making a Lisp
After some experiment with Making a Lisp, I think one of the disadvantages is: I want to get a small interpreter up and running. But it's hard to organize the code such that I can revisit a chunk and replace it. Consequently, it doesn't communicate to the reader.
Knuth writes:
But after gaining experience with WEB, I have come to realize that there is no need to choose once and for all between top-down and bottom-up, because a program is best tough of as a web instead of as a tree. A hierarchical structure is present, but the most important thing about a program is its structural relationships. A complex piece of software consists of simple parts and simple relations between those parts; the programmer's task is to state those parts and those relationships, in whatever order is best for human comprehension—not in some rigidly determined order like top-down or bottom-up.
I don't think I adequately appreciate his examples. Perhaps I need to sit down, and write code snippets on paper (as I write down slips for my Zettelkästen with the chunk name in the top-right corner, etc.), analogous to Knuth's chunks.
4.2. Open Questions
There are a few things I'm uncertain about how to handle with literate programming.
4.2.1. What is the "Best" Order for Human Understanding?
This was never clear to me, and I know myself well enough to know…how I analyze things differs from how anyone else would. Consequently, what's "best" for me differs from what's best for anyone else. But I don't know how to begin, what order to present material. Presumably this requires "big design up front", which does not lend itself to rapid prototyping.
Explaining design choices, well, that doesn't seem to be taught anywhere, ever. Consequently, instead of working through the design in a manner suitable for human beings, it's easier to lapse back into talking to the computer.
4.2.2. "Explanation"?
After thinking about it for a bit, I think one could describe explanations in programming using the following terminology:
- A Problem is what we're trying to solve or implement
- A Goal translates our problem into some informal specification
- A Plan (or pattern) is a "canned solution" to a goal.
An explanation is the dialogue between goals and plans; for example, one explanation could begin with articulating a goal, then presents a plan or decomposes the goal into several sub-goals.
There are different levels of discussion. We could discuss:
- The architecture of the program, as organized into modules or components and their relation to each other
- The design of a class, what it abstracts, the methods it contains
- The design of a method or function
This is not complete, nor exhaustive. There are levels of discussion between architecture and classes (e.g., design patterns could be used to organize discussion of several classes).
But this is not what happens in example literate programs! For
example, in Knuth's adventure
literate program, there are no such
discussions.
One of the difficulties for good writing in literate programs is that the design should be top-down (in the sense that the modules and "important classes" are introduced, their interactions are defined, etc.) but the programming should be bottom-up. An example of this style, see Bob Nystrom's Crafting Interpreters.
For more on the "goal-problem" perspective, see:
- Elliot Soloway,
"Learning to program = learning to construct mechanisms and explanations".
Communications of ACM 29, no.9 (1986) 850–858 doi:10.1145/6592.6594.
4.2.3. Contracts
When I program, I have gotten in the habit of using assert
preconditions and assert
post-conditions. But literate
programming blurs the distinction between functions and
named-chunks. The former can have contracts, the latter should
not.
I suppose for a fragment of code in a chunk, this would become a Hoare triple annotation.
4.2.4. Literate Proofs
I'm writing a manuscript on implementing theorem provers. For this, literate programming is a natural technique. It works fine. But proving properties about the code I've written requires a different style of writing, a different mode of presentation. Because I've introduced a new metalanguage (the input language for the theorem prover used to prove these properties). More generally, whenever defining and implementing a language, I have the problems of smoothly using all the tools at hand.
When I write the code in, say, C…I need something like ACSL to provide an annotated proof of Hoare triples. But proving the Hoare triple annotations hold is done in the text, and is separate from the essay explanation (at least, in my mind). Consequently, I need some "literate proofs" of the Hoare tripled. I suppose I could just shrug and hand it off to Frama-C to check the annotations, but this doesn't seem friendly.
5. Notes on Noweb
I've been tinkering with Noweb to write a literate program in C, ACSL, and Twelf. I think this may be a promising approach when working with multiple languages.
Briefly commands:
- Noweb files have a
.nw
suffix - Extract HTML:
noweave -filter l2h -index -html foo.nw > foo.html
- Extract TeX file:
noweave -x foo.nw > foo.tex
- Extract code:
notangle -L -Rfoo.c foo.nw > foo.c
Formatting:
@
as the first character starts a documentation chunk- In LaTeX,
[[ ... ]]
for embedded code fragments - Use
noweave -delay
to avoid automatically wrapping a document class, begin/end document environment - Start a code chunk with
<<name of chunk>>=
- Insert a code chunk by reference with
<<...>>
- Chunk naming conventions:
- Name chunks inside functions with verbs, usually in imperative mood ("Do this", "Sort that", etc.)
- Name chunks holding declaration prototypes with nouns (e.g., "private function prototypes", "definitions of exported functions", etc.)
- Name top-level "root" chunks after the files they are to represent
(e.g.,
<<foo.c>>=
or<<foo.h>>=
)
6. Knuth's "Structured Programming with GOTO"
There are a few passages worth quoting from Knuth's article on
structured programming with the goto
statement. In the discussion
after example 5a:
If such automatic
goto
elimination procedures are applied to badly structured programs, we can expect the resulting programs to be at least as badly structured. Dijkstra pointed this out already in 1968 [21], saying:The exercise to translate an arbitrary flow diagram more or less mechanically into a jumpless one, however, is not to be recommended. Then the resulting flow diagram cannot be expected to be more transparent than the original one.In other words, we shouldn't merely remove
goto
statements because it's the fashionable thing to do; the presence or absence of goto statements is not really the issue. The underlying structure of the program is what counts, and we want only to avoid usages that somehow clutter up the program. Good structure can be expressed in FORTRAN or COBOL, or even in assembly language, although less clearly and with much more trouble. The real goal is to formulate our programs in such a way that they are easily understood.Program structure refers to the way in which a complex algorithm is built up from successively simpler processes. In most situations this structure can be described very nicely in terms of sequential compositions, conditionals, and simple iterations, together with
case
statements for multiway branches. Undisciplinedgoto
statements make program structure harder to perceive, and they are often symptoms of a poor conceptual formulation. But there has been far too much emphasis ongoto
elimination instead of on the really important issues; people have a natural tendency to set up an easily understood quantitative goal like the abolition of jumps, instead of working directly for a qualitative goal like good program structure. In a similar way, many people have set up "zero population growth" as a goal to be achieved, when they really desire living conditions that are much harder to quantify.Probably the worst mistake any one can make with respect to the subject of
go to
statements is to assume that "structured programming" is achieved by writing programs as we always have and then eliminating thegoto
's. Mostgoto
's shouldn't be there in the first place! What we really want is to conceive of our program in such a way that we rarely even think aboutgoto
statements, because the real need for them hardly ever arises. The language in which we express our ideas has a strong influence on our thought processes. Therefore, Dijkstra [21] asks for more new language features—structures that encourage clear thinking—in order to avoid thegoto
's temptations toward complications.
Emphasis in bold is mine.
6.1. "Structured" — Data and Control
Perhaps one of the ideas which has fallen into obscurity is the use of the term "data structure" refers to something different than what we think of it. There is concern about "well structured data", which sounds odd to modern ears. Knuth explains:
We also need well-structured data; i.e., as we write the program we should have an abstract idea of what each variable means. This idea is also usually describable as an invariant relation, e.g., "\(m\) is the number of items in the table" or "\(x\) is the search argument" or "\(L[i]\) is the number of the root node of node \(t\)'s left subtree, or 0 if this subtree is empty" or "the contents of stack \(S\) are postponed obligations to do such and such."
I confess I have no adequate definition for an "invariant relation", I suspect a good "first approximation" would be an assertion expected to hold.
6.2. Different Levels of Abstraction
When pondering the definition of "structured programming", Knuth argues
it should not rely on goto
statements at all. Instead, it should refer
to thinking at different "levels of abstraction" and the structure of
the program working at these different levels.
Indeed, Dijkstra's original article [23], which gave structured programming its name, never mentioned
goto
statements at all! He directed attention to the critical question, "For what program structures can we give correctness proofs without undue labor, even if the programs get large?" By correctness proofs he explained that he does not mean formal derivations from axioms, he means any sort of proof (formal or informal) that is "sufficiently convincing"; and a proof really means an understanding. By program structure he means data structure as well as control structure.We understand complex things by systematically breaking them into successively simpler parts and understanding how these parts fit together locally. Thus, we have different levels of understanding, and each of those levels corresponds to an abstraction of the detail at the level from which it is composed. For example, at one level of abstraction, we deal with an integer without considering whether it is represented in binary notation or two's complement, etc., while at deeper levels this representation may be important. At more abstract levels the precise value of the integer is not important except as it relates to other data.
(Bold emphasis mine, italics are Knuth's.)
7. References
- Donald Knuth, Literate Programming. CSLI Lecture Notes no 27, 1992.
- Donald Knuth, TeX: The Program. Specifically the forward on "Reading a Web".
- John Cook, Literate programming: presenting code in human order, 6 July 2016 blogpost.
- Elliot Soloway,
"Learning to program = learning to construct mechanisms and explanations".
Communications of ACM 29, no.9 (1986) 850–858 doi:10.1145/6592.6594.