Literate programming: Knuth is doing it wrong (2014)
akkartik.name
akkartik.name
However, this article doesn't mention a newer form of literate programming that has gone mainstream. Notebooks! Whether it's jupyter, colab, whatever the Julia one is called, etc. People use literate programming all the time for data science and machine learning.
I love notebooks, made VS Code extensions for Go (gobook) and for Rust (rustnote). Give them a try if anyone is interested, still lots of features to add, but the source code is standard markdown, so the code results can be saved straight to Github and they render there.
(also obserablejs) for a different take on it.
I have a feeling notebooks are not as popular for javascript since it's quite limiting relative to what a code sandbox can do.
That aside, I completely agree - Observable has brought back a lot of the joy of programming I had when I first started. Congratulations to Bostock and the team.
AFAIK, there are some types of notebooks that allow the use of chunks, but I've never seen people writing in those.
int main(int argc, char **argv) {
<<main-parse-argv>>
// ...
}ipynb files are shit, but Jupyter doesn't have to be. For example, VS Code supports Jupyter without ipynb. This way you can end up with files which, unlike ipynb, are valid in the target language (e.g. python, julia) AND play nice with git, but are still interactive thanks to Jupyter.
In fact, the above realization - that there are IDEs that support Jupyter without ipynb - has been the biggest boost to my productivity in 2021. This came in the form of being able to version my interactive jupyter files.
https://code.visualstudio.com/docs/python/jupyter-support-py
(1) No linking from on cell to another. A sequential execution is assumed or otherwise you need to manually run cells in different order. There might be extensions to help with that. It is not out of the box though.
(2) The documentation language is markdown This does not allow for great technical documents with arbitrary links to arbitrary other parts in the document or inside another notebook. ReStructuredText would have that using link sources and link sinks. org mode can also link to parts in other documents.
(3) I cannot include other notebooks to have it all exported as one whole document or notebook as I can in org mode. In org mode I can even specify at what level some heading should be imported.
(4) The editor in notebooks is not that great compared to any IDE or normal code editor or Emacs.
(5) In a typical JupyterLab environment there is only one programming language per notebook. I know there are other kinds JupyterLab environments, which allow for multiple langs inside the same notebook.
(6) The base format of notebooms is JSON instead of plain text like in org mode. This means diffs are harder to read and version control. Again you need extra tooling for dealing with it well (which does exist).
The literate programming stuff of notebooks has all long been there before notebooks came into existence. It is not like that was the advent of a new form of literate programming. In JupyterLab environments it would take lots of fumbling and tweaking to get something comparable to org + org babel.
As a casual rather than power user, I especially dislike the JSON format of jupyter notebooks.
Maybe I should give it another go with Spacemacs because I like the idea of Org mode. That might make the learning curve gentler. I hadn't considered that possibility at the time.
I don't understand why Emacs's features aren't exposed via context menus. You can always switch to the keyboard later as you get good.
Being emacs, context-menus will also be configurable.
https://github.com/emacs-mirror/emacs/blob/emacs-28/etc/NEWS... https://ruzkuku.com/texts/emacs-mouse.html
For base emacs, the learning curve is not as steep as vim. I wonder if your problem was trying to do everything at once?
Regardless, emacs really is more of a long-term investment. It will take you years to be fully proficient. Personally, I have gone away from using emacs (including org-mode and vim emulation), because it took too much time to keep the configuration shiny.
My main issue with org-mode is that it's only really a first class citizen within emacs. I've not found any vim/vscode/etc. plugins that emulates the experience to satisfactory degree, and that lock-in means I've given up on org-mode.
Now I use org mode almost every day and definitely for all kinds of documentation and notes. It might not be perfect out of the box for formula rendering, but even that can be arranged/configured, so that you can switch to rendered formulas inside your buffer and I have done so in the past.
I have not tried to use Doom Emacs or Spacemacs or whatever flavour. I started with standard Emacs and went with that and still use that. If Doom Emacs or Spacemacs introduce too many new concepts or make the learning curve too steep, perhaps trying standard Emacs will be a better experience.
Not sure what use context menus would have in my personal workflow. Perhaps if they could be opened via keyboard shortcut and then I could select menu items quickly, it would be useful.
Simply run M-x menu-bar-mode, or add (menu-bar-mode 1) into your init.el or config.el
Same syntax for tool-bar-mode. Context menu is ootb for me with doom emacs, so that should just work.
It’s hard to imagine experienced Emacsers wanting to lead a project that solves a problem they don’t have, but the community is very friendly so whoever took it on would get plenty of help.
Mainly I think it is a matter of efficiency...on the implementation side. I mean the hard part about learning something like org-mode (or emacs itself) is conceptual not key bindings.
It's this way because org-mode (or emacs itself) is a powerful tool that users use for decades. The tooling is intended to reward expertise and experience. No amount of context menus will change the fact that the tools are deep and conceptual mastery will take more than a few hours (never mind the thirty seconds the typical app has to engage the user).
Org-mode was written for it's author's use. It was written to get things done. Easy onboarding and massive investment in graphical UI for non-experts wouldn't do that. It requires work to learn. That's true of anything hard.
It's not that I don't sympathize...though it probably can be interpreted that way. It's that using emacs (and org-mode) is analogous to playing the piano, not playing the stereo.
Finally, I while I get the appeal of things like spacemacs, I think it's mostly a distraction. The documentation and tooling is by it's nature derivative...it is by it's nature extra work. Even worse it's extra work that's driven by ideology. Worser still, it's extra work driven by the ideology of Vim versus Emacs. And worst of all, it's rational is emacs-is-too-hard-to-learn.
Emacs is hard to learn, but not too hard. It's harder to learn than a sixteen week college course. But that's most things in the working world.
- cells are linked through a dependency graph, so the output you see is independent of the order in which the cells appear;
- each notebook is backed by a plain Julia module, so you can use version control, edit in a real editor instead of only in the browser, and import other notebooks;
- You can create interaction² using HTML inputs, Julia wrappers over those, or go crazy with your own custom JavaScript controls;
- It’s easy to embed LaTeX into the MarkDown, and you can use HTML, so any hyperlinking works.
1 ‘An introduction to pluto’. LWN. Available from: https://lwn.net/Articles/835930/
2 ‘PlutoUI demonstration’. Available from: https://pluton.lee-phillips.org/sliders/uiDemo.htmlI wish it had a sequential mode.
2) I can't understand the desire for RST over Markdown. Between simplicity and adoption, Markdown beats almost all mark-up languages -- maybe even html.
3) You can use the `run` command to link notebook executions and use Markdown linking to link the documentation.
4) This isn't true. I use vim in Jupyter. There's plugins for emacs too. Also, many editors like VSCode can edit notebooks directly.
5) This is a weird complaint. I can imagine a few corner cases where mixing languages in a notebook would be nice...but the concept of a multi-language Jupyter kernel doesn't make much sense from a practical standpoint.
6) There are diff tools such as `nbdime`. Between those and a few git commit hooks to leave your notebooks in a consistent execution state, merging versions notebooks isn't any worse than other files.
In fact, the execution graph logic is handled by the user. When it gets too complex, it's a sign that the notebook itself is too complex, and it's time to move a lot of code into libraries. Notebooks /should/ be conceptually simple.
Then again, I bounced off emacs after deciding the complexity didn't actually justify the endpoint: I was already able to write and maintain code with much simpler tools, so why bother? I can use the party of my brain that would otherwise be used for key bindings for other things. So, mileage varies.
- python3 (OK, mostly a given, though not on Windows)
- pip and depending on your requirements you might need Poetry or Pipenv or whatever else
- Jupyter ecosystem (JupyterLab, Jupyter/notebook server and all their dependencies)
- some extensions for JupyterLab. Those could be frontend and backend. If frontend, then you will also need to have NodeJS on your machine, to have NPM, to have tsc to be able to build JupyterLab with the extension installed.
Quite a few parts there as well.
Cognitive load when using the notebook is very low compared to emacs. As has been remarked on often, emacs is probably fine if you've been using it for thirty years, but having to keep track of different key strokes for copy paste depending on what window you're in (emacs vs the rest of the world) is pointless cognitive load. The idioms are so sprawling and out of sync with everything else that the editor is an obstacle in the way to doing what I want.
And while (historically) that perspective may just paint me as a noob, I'll just say that no amount of memorizing editor functionality or micro optimizing configurations will ever help me prove a theorem. If there's another tool that gets the job done and doesn't want to fight me before/while I use it, then it's the better tool.
(1) This is better to me, because it makes notebooks more consistent and easier to jump into. I read english prose sequentially, why not code?
(2) You can do this in all environments I've used with <a> tags.
(3) this would be nice, but I'm not sure it's worth the tradeoff because the editing environment wouldn't correspond as well to the exported document.
(4) true, but I have a vim plugin that make it better
(5) also true, but this is a good thing for simplicity. I don't want to have to keep track of the lexical scope of multiple language environments
(6) There are tools for this, but yeah it's not great. The main issue is saving outputs
It sounds like the goals of notebooks vs org mode are slightly different, at least the way I use notebooks.
For me, notebooks are a tool for sacrificing control and programming flexibility in exchange for easier collaboration and visualization. It's harder to write structured code, but it's much easier to share with a large number of semi-technical people.
As a counterexample, some things are not important but have to be mentioned. In English, these things end up in the appendix. In Pluto notebooks, these things can be moved to the bottom.
Another benefit of less strict ordering is when debugging. Sometimes cell A fails because of cell B. It can be useful for debugging to temporarily put these cells next to each other
In a top to bottom typical notebook, you will have to run the notebook from the top. There is a menu item for that, of course. However, what if you also have some cells in there, which do not have to be run? They will waste time unnecessarily or even worse, will change the result. So you will need to take a look at each cell to make sure you really can run everything sequentially. Either the notebook has to be created to make sure, that running top to bottom always works and gives the same result, or you will have to look at everything. When code segments can define their dependence on other code segments, they can be designed to that it does not matter which one you run, it will always give the intended result.
The Julia one is called jupyter.
Jupyter stands for Julia, Python, TeX and R.
1 ‘An introduction to pluto’. LWN. Available from: https://lwn.net/Articles/835930/Notebooks are used for small snippets, many of them trivial. The comments usually tell you what the buggy snippet is supposed to to. They are more like Powerpoint.
The focus on a notebook is the report, not the code. In practice this means that the report structure dictates code structure, not the other way round.
By contrast, the focus of literate programming is the code. Any "reporting" done is strictly to enhance the code, and not the other way round.
I've seen far too many codebases that should have been "code generating a report" reverse that logic and implemented as notebooks instead, in the expectation that the underlying code can simply be exported from the report. Good luck if you need to work on someone's code of that kind. It's its own special kind of hell.
With "literate Programming", it is one product, the program! Ideally, it is just readable code, code that human can read literally. Since human mind works differently from computer, so the literate programming environment need transform the literate code into a form that compiler can optimize. I believe this idea is inside Knuth's LP. But early attempt is always limited by technology. For example, Knuth's system is based on token parser. Token is not the unit of comprehension for human. Thus he can't do much with the Pascal part other than "tangling" the code as is. Then he had to supplement his goal with English or "document". But if we start to develop the framework beyond "tangle", we'll see a new page in literate programming.
* The "Notebooks" (the noun) implies we are emphasizing the product as documents. However, nothing prevent people to use "Notebooks" to program. Then immediately one will see how limited it is. "Notebooks" does not "tangle" code. Really, there is not much you can do with the code part. For example, try use the "Notebook" form on a C program.
Sorry, that's not correct. When writing a literate program you should also include a chunk containing a Makefile. Extract the Makefile and let it construct the program.
I did this with the whole Clojure programming language. The whole source code for all of Clojure, as well as the test cases, are in the PDF. The sequence (from scratch is):
Extract the Makefile from the Latex document. Run the Makefile. This:
1) extracts the code and tests
2) compiles the code
3) runs the tests
4) recreates the PDF from the Latex
So you edit the Latex text and/or code with any editor and then re-run the Makefile. This
will rebuild your program, runs the tests, and re-makes the PDF.If you have the PDF open next to your editor it is likely that the PDF viewer will reload the changes immediately.
This ensures that (a) your program compiles, (b) your program passes tests, and (c) the PDF builds correctly.
I've been working on literate programs for years. See https://en.wikipedia.org/wiki/Axiom_(computer_algebra_system.... I've even given a talk on the subject See https://www.youtube.com/watch?v=Av0PQDVTP4A).
I find literate programming to be the MOST important change in programming since I started in 1970.
Java wants specially named paths to files. The Makefile can create the directory structure and use the "tangle" program to extract the files to the correct place. That's what I did in the Clojure example.
The program scans the Latex looking for named "chunk" blocks. Each named block is put in a hash table under the name. Once the scan is complete you use a provided command line argument to extract the named chunk to a file. Of course, the chunk can include other chunks and these get extracted and expanded inline. So
tanglec myliteratefile.tex achunkname > myfile.lang
This creates a hash table from "myliteratefile.tex" containing named chunks, then prints the chunk "achunkname" to stdout (recursively expanding chunks embedded in other chunks). The Makefile can extract Java files and put them in the proper directories.
It really is a trivial C program. Make a hash, print a hash key. This would be a simple interview test question.
(Actually I use Lisp, mostly because I always code in Lisp these days, but lets not start that war.)
Pick up a physics textbook. Copy down all of the equations. Throw away the textbook and learn physics only using the equations. That's what programmers do. They write down the "equations" but fail to explain the ideas.
Equations, like programs, are really just an "iconic form" of an idea. The idea, however, lives in the surrounding text.
You're not __directly__ editing the "nicer" representation.
Mr. Daly, your chat was formative for me as I made the switch to literate programming (primarily with org/org-babel) in most of my work and personal projects. While I'd love to see what was in your slides, I'm glad the talk turned out "off the cuff." Thank you!
I also intended to give a live demonstration of literate code development, as outlined above.
It only takes a minute or so to start from the PDF to create a running, tested version of Clojure running in Java, along with a new PDF. I intended to demonstrate changing both code and data in real time.
The only way to correctly change code is to deeply understand the implications of the change. This requires a deep understanding of the code and an awareness of the big picture. Yet the "why" of code is rarely ever written down in standard programming practice. The goal is only to elaborate the "how" so the machine can perform the task. The programmer communicates with the machine.
Literate programming, as used in Axiom, is an attempt to communicate with other users, developers, and researchers in addition to the machine. The goal is to have the program read like a story so that others can understand the rational, the theory, the choices, the implications, and the implementation context as well as the "how".
This code is intended to live forever but it is highly probable that you will not. Write to communicate with the next person to pick up the torch. When you explore code, write down what you learn. When you change code, explain why you made your choices. When you write new code explain what others need to know to maintain it.
On the other hand, all the LP programs I've seen were built by people who care about documentation. Perhaps we would have managed to keep things up to date even without LP. There may be a selection bias here.
Either way, I'd say you don't know what you're talking about when it comes to daly's Axiom (http://axiom-developer.org). It doesn't just end up with outdated comments, that's a serious charge to level without concrete evidence.
Similarly, I'd be extremely interested in outdated comments in https://github.com/akkartik/mu/tree/main/linux/bootstrap which is written in the LP style I described in OP. I consider outdated comments to be bugs.
IMO, LP-style programming doesn’t change that. If the problem happens less with programs written in LP-style, I agree with you chances are it’s because people who write in that style tend to be more careful.
What would change it is to require programmers to supply proofs about their code. Unit tests don’t guarantee that, but have the benefit of being easier to write.
Unit tests or proofs are extremely useful. You should still have them in Literate programs! They're complementary tools.
My Literate system does explicitly assume tests unlike earlier systems I was dissatisfied with. Check out all the tests in https://github.com/akkartik/mu/tree/main/linux/bootstrap. For example, https://github.com/akkartik/mu/blob/main/linux/bootstrap/013...
You get it. I get it. Unfortunately every intro class inculcates generous commenting, a "best practice" further promoted by corporate proceduralists and mediocre programmers who inexorably become managers.
A comment longer than 160 characters is always weak-sauce justification for logic written awkwardly.
Commenting too much in the code simply makes my work impossibly hard because I need to juggle with contradictory statements. I ve found that always committing with a ticket reference and keeping tickets forever allows me to go back sometimes decades ago to understand the problem they were solving at the time, for each layer of the code im looking at (each line having an history).
I read comments now with suspicion as sometimes the comment is a commit 20 years ago and the associated line is 5 years old. Should it have always been kept in sync ? Yes. Could it have been ? I've never seen it done properly.
Yet to me, literate programming is certainly the most important thing that came out of the TeX project. Not only has it enabled me to write and maintain programs faster and more reliably than ever before, and been one of my greatest sources of joy since the 1980s -- it has actually been indispensable at times. Some of my major programs, such as the MMIX meta-simulator, could not have been written with any other methodology that I've ever heard of. The complexity was simply too daunting for my limited brain to handle; without literate programming, the whole enterprise would have flopped miserably.
If people discover nice ways to use the newfangled multithreaded machines, I would expect the discovery to come from people who routinely use literate programming. Literate programming is what you need to rise above the ordinary level of achievement. But I don't believe in forcing ideas on anybody.
-- Knuth, DonaldNotebooks are a direct evolution of REPLs and at the time of writing (Knuth published his paper on literate programming in 1984), REPLs have been well known (they had been around since 1970s). Yet there is not a single mention of them in the original paper, nor any prediction how literate-programming-capable notebooks could look like. At the time, these were two different concepts each serving its own need (documentation vs exploratory programming).
To me, literate programming is massive productivity left on the table. I love it.
I love it so much I developed https://webcode.run to extend observable to the backend with a fresh retake of a serverless compute environment. I have a working OAUTH server "hosted" here: https://observablehq.com/@endpointservices/auth if you are concerned this does not scale to interesting infra. You can eject source code as ES6 modules and publish to NPM which I have done for generally useful tools like the first REDIS client for web https://observablehq.com/@tomlarkworthy/redis
Should we try to write code that is primarily meant to be read? YES! Oh god yes! Code is read 1000x more than it is written. But that doesn't mean "stuff the code with comments".
Primarily, it means write code that is clear, concise, conveys the meaning, is logically structured, is not "optimized" into obscurity before even the first performance measurement has been done, and doesn't implement something just to fulfill some dogmatic paradigm.
Once that's in the bank, we can start using comments where they are needed: Explain Functions that are part of the interface, explain functions that have complex logic, explain imports that aren't part of the stdlib or well known 3rd party libraries. I don't need a comment telling me what `import requests´ is needed for. I certainly do need a comment telling me what the hell `from apputil.tools import tvectorization` does.
Collectively, we already fail at the first stage, writing code, which is well structured and through its structure conveys meaning and understanding. It is rare, that I see this being done in a way, that makes me think: "Ah, great code!" Perhaps in SICP or in some conference talks I see it more often, when people know, they are going to present it and it should be done really well. When I see some typical algorithm video however, people seem to get a kick out of explaining comparatively unreadable code, instead of first structuring it really well. Structuring code really well is an art and certainly one cannot always do as great as some of the greats, who had years time to improve some snippet.
Code being read 1000x more than written might be an exaggeration, or over-generalization, as not all code will be written to last years, however, I agree with the basic idea it implies.
I do not mind there being long comments in the code. I have done so myself, as long as they are useful for an in-depth understanding. You might get into a situation, where you work with an API, which when the concept that is implemented behind it would make things clear, but you do not want to assume understanding of those concepts by the reader of your code. Of course you could say, that they must read the docs of those concepts then, but what if that is a paper, which takes days to read and understand yourself? Maybe you can put very helpful explanation in some comments, to safe others lots of time.
Me neither. If the code is good, its author may put in some Haikus about his thoughts about the meaning of life for all I care. If it's to much, I can use my IDE and just hide it.
What bothers me isn't lots of comments per-se, but when more emphasis is put on having lots of comments, than on having good, readable code.
Comments tend not to be maintained with the code. Block-comments up-source often describe general philosophy covering functions way down-source, that may or may not be relevant; does this one-line change on line 200 impact the philosophy comment on line 20? Who cares. Fix the bug, and let's go home.
So comments are needed only if the code is tricksy. Tricksy code is to be avoided; code should generally be clear. If it has to be tricksy, then a comment is justified to explain why it isn't clear (otherwise someone will come along and de-tricksify it in the interests of clarity).
Code is written for people, not for computers. Otherwise we'd be writing programs in hex (which is how I had to write my first-ever program).
If the code in question is part of the pkg/module/crate/whatever's API, aka. someone else may have to use it some day, it should be commented regardless of how complex it is. Whatever interacts with "the outside world" should be labeled, that's true in code, and that's true with the big red emergency stop button found on heavy machinery. It doesn't matter how obvious the usage is.
No, the API should be documented, which is orthogonal to the presence or absence of comments inside the code.
There's another thread on the front page about literate programming; I suspect that doc-comments amount to an effort to weave code and documentation together, in a knuthian manner, to kind-of automate literate programming. It doesn't work, AFAICS. You get bloated code and bad docs.
doc-comments are good because it explains well what the class/method/whatever are there for, sometimes with examples, that helps understand the code.
But I hate that they use so much space inside the source code that I have to scroll hundreds of lines to read the code. And those doc-comments are filled with markup that makes it harder to read.
I would like a literate a system that generates source code that is well formatted and where the only comments are what the function does and some inner function comment where the code is complex. Every other lengthy and detailed description should be inside the produced pdf/html.
Languages like Java, Python or Golang enable inline documentation which can then be used by standardized tools (Javadoc, Docstrings, Godoc).
eg.
https://pkg.go.dev/flag@go1.17.6#PrintDefaults
The entire documentation of this function (including HTML) is generated by the Godoc tool reading comments which conform to a certain convention.
https://cs.opensource.google/go/go/+/refs/tags/go1.17.6:src/...
There is literally no downside to this. The code and its documentation are in the same file, its much simpler for developers to update it when there is a change, and its easy to generate documentation directly from source on the fly.
Apart from new programmers who put those comments in to remind themselves what each line does, I haven't seen "comment-noise" since... 2005 sounds right.
If it was common I'd be able to go on GitHub and have my searches spoiled by comments. But instead on GitHub it's the opposite, as it is in my working life: people have gone to a "we don't write comments" default position.
Getting a balance is necessary, but I'd rather we had programmers over-commenting than under-commenting, especially for open source projects where "read the code" is used to mean "we don't need documentation as our code's so great".
It also depends who is going to read the code. Other experienced programmers need different comments than when my code is going to be read by developers who are unfamiliar with the codebase and didn't expect to ever touch it.
I agree with what you say but I encourage people to add comments and then remove them later if not needed, rather than omit them entirely.
Because that's noise that makes reading the code more difficult. Creating a separate markup file that provides comments (even overlapping) that are associated to files and lines, for display in an IDE window seems like the logical way to go from the current style of inlining comments into code.
I'm not sure why that matters to utility.
If you don't check the documentation/comments and code in a changeset, you are going to get drift. This is regardless of the documentation methodology. How poorly developers maintain code is a constant in the case of drift (lack of rigor), so the method is irrelevant, except in how descriptive it can be.
A complex application is a nested logical structure tied to orthogonal business requirements. This is why READMEs and docs folders exist, alongside large block comments, hidden amongst the code. How to best describe code is definitely not to inline it, as that's too limiting for what is to be described.
That way, the thoughts of the programmer become more linear, and the code becomes easier to read.
Another article on this topic I recommend is "Code Is Not Literature," from Peter Siebel: https://gigamonkeys.com/code-reading/
Peter Siebel is right. However, he is talking about code as it is written today.
It's one thing to include some background notes around a single algorithm, it's something else entirely to document an entire application.
The missing link is the lack of meta-representation and abstraction tools.
Text is very poor way to show relationships, and most code is more like a tree structure. IDEs have gotten better at representing this, but they still tend to show text files as primary and relationships as secondary.
More text in/around the text files is definitely not the answer. Structural representations - dynamic useful ones, not bureaucratic nonsense like UML - and smarter searching and filtering are more likely to be helpful, with associated text descriptions for key concepts.
Seibel seems to have taken the literate part of literate-programming too literately with his reading club and not expecting to implement at least a toy version of the code to understand it.
This is why I compared it to a math book, in that it is also a book that is read but to really understand something one must work out some of the problems. I did not mean that a program is like a math book, I meant that a literate-program is like a math book because it adds some context to the math inside. I think a program itself is like math with no context where you have to reverse engineer what its doing by guessing what the person who wrote it was trying to do.
The first point in the post is that many (non-Knuth) LP systems provide typesetting, but allowing arbitrary code re-ordering (as in Knuth's WEB/CWEB) is actually the more important feature. This is a good point.
But next it faults Knuth for putting #include statements at the top of the program, instead of using his system's reordering feature to have an "⟨includes⟩" section that is later filled in when each #include is needed (the way he does for "⟨Type definitions⟩" and "⟨Global variables⟩" in those examples). This slightly misunderstands the purpose of literate programming, I think.
As far as Knuth is concerned, the main idea in literate programming is an attitude, an orientation: treating programming as writing, for a human reader. (This particularly makes sense for him as he is primarily, in a very deep way, a writer.) Now, writing is always (best) done with a specific reader in mind: you assume the reader has a certain background/prerequisites: some things that don't need explaining, and some things that do. Depending on the reader you're targeting (e.g., yourself a few years from now), and how polished you're trying to make your presentation, you may well choose to take it for granted and not bother explaining that a C program will have some obvious #includes at the top. Take it for granted, as understood between friends.
From this point of view, as long as you're writing for a human reader, it is literate programming whether you're at the extreme of explaining every part of every line of code, to a reader who does not even know the language, or at the other extreme of "here's my program: ⟨the entire code of the program⟩": it's only a matter of degree, how much you want to explain. [This is actually one of the misconceptions about LP: it doesn't necessarily require you to write lots of comments, or to explain everything verbosely; the main thing is to treat the activity as writing, and explain to whatever degree it makes sense given your intended reader. In Knuth's own TeX program http://mirrors.ctan.org/info/knuth-pdf/tex/tex.pdf , the one he came up with literate programming for, there are sections where he explains things very thoroughly but see also section §315 where, after explaining, he says "This is easier to program than to explain", and §69 where he says "Readers who like puzzles might enjoy trying to figure out how this tricky code works; therefore no explanation will be given" :-)]
A few other criticisms in the post are understandable given unfamiliarity with some further context about Knuth's work and these programs:
• Many of the examples being criticized are from programs that Knuth wrote for himself, as he mentions at the top of https://cs.stanford.edu/~knuth/programs.html — obviously the standard of polish and degree of explaining will be different when something is written up for public presentation rather than for oneself. (As I said, these examples also show that to write and enjoy LP you don't need to overdo it, feel free to explain only as much as you want.)
• An example is criticized for the fact that "GraphBase data structures" are not described in-line in the program, but again, as the webpage says, this is a program that uses "the conventions and library of The Stanford GraphBase" — Knuth has published an entire book about these data structures, so it isn't necessary (or realistic) to re-describe them inline. In fact, many of Knuth's programs can be criticized for being monolithic rather than modular, but the Stanford GraphBase is a rare case of Knuth actually factoring out a library for use in many programs.
• There's a criticism for "a steep jump across several levels of abstraction", but, contrary to common opinion, Knuth has repeatedly mentioned this as a good thing:
> "I have felt for a long time that a talent for programming consists largely of the ability to switch readily from microscopic to macroscopic views of things, i.e., to change levels of abstraction fluently." — 1974, Structured Programming with Go To Statements (http://www.kohala.com/start/papers.others/knuth.dec74.html, https://pic.plover.com/knuth-GOTO.pdf#page=32 )
> "I believe the thing that marks a computer scientist most is an ability to jump across levels of abstraction: We see the small and the large and several things in between as a continuum, and we don't even notice that we're jumping levels. — 2013 (https://programmingphilosophy.org/posts/jumping-across-level...)
> "after decades of observation I’ve come to believe that one particular talent stands out among the world-class programmers I’ve known— namely, an ability to move effortlessly between different levels of abstraction […] A programmer must deal with high-level concepts related to a problem area, with low-level concepts related to basic steps of computation, and with numerous levels in between. We represent reality by creating structures that are composed of progressively simpler and simpler parts. We don’t only need to understand how those parts fit together; we also need to be able somehow to envision the whole show — to see everything in the large while seeing it simultaneously in the small and in the middle. Without blinking an eye, we need to understand why a major goal can be accomplished if we begin by increasing the contents of a lowly computer register by 1. The best way to enhance our level-jumping skills is to exercise them frequently." — Foreword to The MMIX Supplement, published 2015.
• The second major criticism in the post is that the woven program (the readable output of the LP system: a typeset PDF file or book or whatever) is "dead". This is reasonable given the majority of readers, including myself in the past. But in fact, the intention is the very opposite of "we can't make changes to it while we work on the codebase. Everybody reads a pdf about a program at most once, when they first encounter it." For Knuth, the printed/woven version is the program, the one he thinks of as canonical and the one he uses whenever debugging or making changes to the program (with pencil on paper, I think). It serves him well: here he is in 2021:
> While I was preparing this round of updates, I was overjoyed to see how well the philosophy of literate programming has facilitated everything. This multifaceted program [TeX] was written 40 years ago, yet I could still get back into TeX’s darkest corners without trouble, just by rereading [B] and using its index and mini-indexes! — https://tug.org/tugboat/tb42-1/tb130knuth-tuneup21.pdf
And unlike the common criticism (not present in this post, to be fair) of the author's order not being what a reader may want, in fact a book is not meant to be read linearly. ("Books are not scrolls. […] Books are random access" — Manuel Blum, https://www.cs.cmu.edu/~mblum/research/pdf/grad.html) Knuth also recommends reading in whatever order makes sense; he thinks that his indexes are enough to enable this, though readers like us may disagree.
His idea of reading (a book, or a program) does include "poke at it and run what-if experiments", just that he does it in his head. See Peter Seibel's post at https://gigamonkeys.com/code-reading/ where he describes how Knuth was one of the only two interviewees in his Coders at Work who do a lot of reading other people's code, and he also includes some detail of how Knuth read some program. (See also this great post by Dylan Lederle-Ensign on Knuth's reading practice: http://www.dlederle.com/2018/11/25/reading-programs.html In fact, one of these programs Knuth "read" this way while spending a long time in hospital with a broken arm, he says somewhere IIRC.) So though I agree with the ideal of "viewing live, modifiable, interactive code", the intention of Knuth-style woven output is far from "passive reading" as the post says.
Nevertheless, given most readers today (probably this was true in 1980s too, to be fair), the post is probably right that given a typeset version people will read it passively, so overall this criticism is probably worth making, at least as a warning.
There are lots of other criticisms that can be made about how Knuth does literate programming (here's a discussion I found just now: https://wiki.c2.com/?LongFunctionsByDonaldKnuth), and I've made quite a few myself, but this comment is already too long (and too late). For the "ADVENT[URE]" program in particular, see this discussion from about a year ago: https://news.ycombinator.com/item?id=25597842
For example:
cell\* run(string s) {
stringstream in(s);
return run(in);
}
What is "s"?!What is "in"?!
What the hell does "run" do?!
All this code commenting (which is bound to deviate from the implementation!) but no thought into naming and structuring code in an easy to understand fashion.
Replace "run()" with what it actually does even if it means naming the function "replaceAllNamesInTheInputStringByDoingAFancyComputation()" or whatever.
It's all better then "run()".
Replace the variables with easy to understand names!
I'd be mad as hell if my colleagues wrote that code.
Another thing to consider is the very verbose names can be something that people actively avoid for clean code, and you could end up with spaghetti code to make it both readable and verbose (imagine someone doing this for years and then handing the code off to someone when they leave because they were so pigeonholed in that system that no one else could easily collaborate).
Downstream, you can end up with tech debt instead of optimally refactored code as people struggle to do something of a smaller scope. And if every tiny thing is unit tested, it gets even harder to change without expanding the scope of work.
Math code is an extreme example of where verbosity can quickly become detrimental.
"Clean" code is often an excuse for lazy, short names which need a comment to explain them. Consider this completely fabricated example:
// calculates the area of a rectangle with sides a and b, u being true for metric, false for imperial
int rarea(int a, int b, bool u);
versus
int rectangle_area(int side_a, int side_b, bool metric);
which more or less doesnt even need a comment anymore.
"Clean code" people want short names, massive comments that can be collapsed, so that from really far away, it looks neat. Its a terrible beginner mistake I see time and time again, and it shouldnt be defended.
If you write a math formula, how is writing "side_a" or "phi" worse than "a" and "p"?
Its a made-up example, this discussion is about style, not about semantics. To please those who would like to see a semantically improved version: (C++)
enum class Unit {
Meter,
Inch,
Foot,
// ...
};
template<Unit U>
struct UnitValue {
double value;
};
template<Unit U>
UnitValue<U> rectangle_area(UnitValue<U> side_a, UnitValue<U> side_b);
// invocation example
constexpr Unit unit = Unit::Meter;
UnitValue<unit> result = rectangle_area<unit>({5.3}, {1.5});
std::cout << result.value << std::endl;
There is still room for improvement, maybe some concepts to further generify it, some more comments, maybe a PDF with full documentation, ...I would, however, point out that to call this function, you need to separately compute lengths of sides. Where do those come from? How are coordinates stored? How are you wiring all this up? What happens when you change the data to support more dimensions, or to use memory pools, or to add new shapes?
To be clear, a lot of math needs a ton of complex code, and you also often have algorithms optimized for performance, approximation, numerical stability, etc. You can encode your understanding of what’s going on in the naming and break it into chunks so it’s readable, but that’s not necessarily a good thing.
Write some code to compute the intersecting earth-surface geometry of the projected frustum far planes from two satellites’ cameras at some point in time. Who’s using this code, is it a library? How do you abstract it? Who are the consumers of the code, will they ever change it or only use a part of it and want to refactor? Is it going to be a lot of work to refactor and are they just going to inject what they need into an evolving system of glue code? If I have to call it n times, how can I pass in and reuse memory to avoid memory penalties? What happens to names when we have to reuse symbols?
Edit: probably not obvious, but you can probably elegantly describe what you’d do with ideal inputs, but getting those inputs is the particularly hard part.
When you pick up your pencil to do some manipulations on that formula, you’ll wish you’d chosen the one-letter variables. Also, single character variables, perhaps with different fonts, subscripts, or other decorations, make it easier to see the structure of the equations. That’s why we use them in math.
For those of us who use a pencil to manipulate our code, to see how it might be transformed, the value of short variable names is the same—we’re often going back and forth between code and math. That’s why languages like Julia that let you use Unicode identifiers are so cool.
More on my worldview regarding readability: http://akkartik.name/post/readable-bad
In particular:
"In practice, a series of locally well-chosen names gradually end up in overall cacophony. A program with a small harmonious vocabulary of names consistently used is hugely effective regardless of whether its types and variables are visibly distinguished. To put it another way, the readability of a program can be hugely enhanced by the names it doesn't use."
You'll probably not like Smalltalk programs either, which almost entirely have names like `aString`. These days I don't get hung up on style. Everyone has their own style, and that's good. When I read a program that does something useful to me, I try to adopt its style.
Given in this specific case the reader (of the article rather than the comment you're replying to) already has the context that it's an interpreter implementation, the meaning of run() was immediately obvious to me, but if I'd missed that sentence in the article I suspect I'd've reacted rather differently.
I might remember to email you later but if there's somewhere on IRC you're usually connected to telling me where that is will significantly increase the odds of my actually getting around to providing feedback if you'd like that.
Yes please, I'm on akkartik on irc.esper.net, irc.libera.chat and irc.tilde.chat.
More options: http://akkartik.name/contact
Within any single source file, I will assume that you take some care to arrange parts in some sensible order. Why is it such a ridiculous idea to do this for the files themselves?
IMHO, this is one of the three fundamental mistakes of literate programming:
1. Not everyone is an author or a teacher like Knuth is. Writing is actually hard.
2. A program is obviously not a linear thing. It is a network of interactions. The only way you can make it linear is to tell the tale of what happens in a given set of circumstances.
3. Docs and comments are prone to bitrot. You absolutely want to minimize then.
Literate programming in the sense of documenting a program like a monograph is too one-dimensional.
I would like to see an emphasis on test inputs/outputs instead. Given all the Test vectors the actual code becomes irrelevant. It seems that we focus on the code because it used to be the bottleneck, plus it’s the densest piece of information in the system…and takes less paper to print.
In a world where modules must be constantly integrated, I’d rather read the test traces than the code itself.
Good programmers can simulate the code in their heads and internally generate the traces…but presumably the point of literate programming should be to remove that burden from the reader.
Pluto is exciting because - it provides nice text formatting easily via MarkDown and simple production of graphics - it allows you to build a data flow out of small understandable steps
and, unusually, - it understands the dependency graph between cells of computation. That means can rearrange them as you see fit but they will still execute in the right order. Moreover, it makes it so everybody sees the results of executing things in the right order. This is worlds different from most notebook systems
- you can embed HTML controls directly in your notebook so that you get reactive computation
This meets many of the goals Kartik was talking about in his blog.
Don't tell me tautological comments("add X and Y"), but I want to know what the overall strategy of your algorithm is. Is it flawed? Do you like it? What needs improving? Why did you choose this data structure over that one? It doesn't have to be much detail, but having a chunk of text to read is some of the best onboarding you can get for a new piece of code.
Code with comments that simply write down what it is supposed to do, is not documented properly. I can now see what it does in a documentation generator, yes, but the moment I need to work on that code it's just an opaque piece of crap.
Knuth didn't take us to the promised land, but he did show us a path to it.
My favorite: `void setTimeout(int time); // in milliseconds` -> `void setTimeout(std::chrono::milliseconds time);` which adds unit-safety and eschews comments that can rot.
I'd love to know about similarly documented code bases. I have femtolisp on my bucket list.
Semi-literate systems are not LP.
The essence of LP is to not be constrained by some language's compiler when deciding how to explain a system to another person.
Now, lots of languages today are nice enough that you can work within them and still do LP. So it is certainly possible for a program written in a semi-literate system to be LP. But that is a property of the _program_, not "Literate Haskell" or whatever.
Jonesforth is written in Assembly. Assembly is not nice enough to write a Literate program in directly.
It is certainly possible to write beautiful programs without LP. But I think LP is a valuable idea that we should at least understand the boundaries of. If you think Jonesforth is LP, you don't understand LP.
For tangling I use lmt [2], as it works with Markdown and also play nice with Emanote [3] (full syntax highlighting inside the code blocks.). That means all my "dotfiles" are inside my Zettelkasten [4] and can be navigated like any other note I have.
[1]: https://nixos.org/
[2]: https://github.com/driusan/lmt
I'm ... invariable a little ambivalent about doing that, but I do it rarely enough and the times I have have come in handy often enough later that I think it's one of those things where "ask yourself twice if you really want to do this, but don't never do it" applies.
https://observablehq.com/@observablehq/how-observable-runs
You can totally put imports at the bottom!
For me the intent there was effectively addressing one of c's weaknesses, which is lack of namespaces and clear import statements.
It is the python equivalent of doing
from module import fun1, fun2
instead of from module import *
such that when the reader encounters fun1 later in the code, a simple glance at the top of the file should make it abundantly clear where fun1 came from, saving you a wild goose chase grepping into your system libraries."Oh, but, I can use my super expensive IDE's code-hunting plugin to get a 'where is this defined' popup when I hover with the mouse for a couple of seconds bla bla bla" you might say.
True. But then you'd be missing the whole point of literate programming, and the actual spirit of "you should be able to read the code in bed like a novel", which the author seems to have taken a bit too literally.
To me, literate programming could be effectively reframed as "code that is so self explanatory and clear, even in the presence of complex external dependencies, that it could be easily fixed _in nano_ within seconds, without ever having to go hunting outside that file".
PS: Unrelated to the above rant, on the other hand, some people seem to think that literate programming == jupyter notebooks or more generally the whole "cram everything in a single file and comment heavily to create a report" style of coding. No. In fact, this is as far away from what literate programming is about in spirit as could possibly be.
He writes code mostly for instructional purposes, for humans to read, I would think.
Literate programming makes sense in that context. And it is true that to create a truly maintainable program you need to provide good documentation, maybe literate programming is the best way to accomplish that.
However I feel I don't have the time to invest in producing great documentation. I write code the best I can and my goal is to finish a big project in a timely manner. I feel documenting everything while the program is still evolving would be wasteful because then the documentation should be continually revised and typically it is not.
A similar issue applies to unit-testing. If I need to change my program I also need to rewrite the unit-tests. "Testability" is a great goal for software and so is "maintainability". But the most important quality of software is "does it do what it should, already this year".
If I wrote a blog-post or a technical article I would put great care into how I best present the software both in code and in documentation around it. But mostly I just write programs for the computer to execute, not for people to read about it.
The ultimate example of Literate Programming is writing a book about your program. I had a book once called "How Tomcat Works", about the web-server called "Tomcat". A great book explaining in detail how Tomcat does (did? )work. But who of us have the time to write a book?
My priorities are:
1. Write great code that does what it should
2. Write some API documentation about it so I and others can understand how to use it.
3. (The least important) Document and explain how the code works.
But I don't like the idea that the prose you read is out-of-order relative to the code you write.
I prefer a modern language that reads well in-order, that you can read as an outline. That way, you can use tree depth for significance.
Important stuff on the first or second level. Less important stuff under a sub heading, because it doesn't contribute to the narrative.
What this lets him do is talk, in detail, about those fragments of a program in the academic paper that the program is about, with the commentary interspersed among those fragments.
Then the paper can present how those fragments are combined together to form the next unit, and so on.
It's a complete non-starter for production code; anything written by two or more programmers whose goal isn't to have a working system documented in detail by an academic paper from whose markup source code it pops out as a side effect.
The goal is not an "academic paper"; his experience (and that of others who have seriously tried LP) is that it helps with actual writing of programs, less time spent debugging, etc.
Yes, there are challenges with two or more programmers, but nothing unsurmountable. See "Literate Programming on a Team Project" (https://www.cs.princeton.edu/techreports/1991/302.pdf coauthored by Norman Ramsey, who later developed noweb) and some stories like https://news.ycombinator.com/item?id=17484452 (and https://github.com/thi-ng/geom which went from LP to conventional).
That's why he called it "The Art of Computer Programming", not "Engineering Principles for Computer Programming".
The effect of this simple shift of emphasis can be so profound as to change one's whole approach to programming. Under the literate programming paradigm, the central activity of programming becomes that of conveying meaning to other intelligent beings rather than merely convincing the computer to behave in a particular way. It is the difference between performing and exposing a magic trick.
--Ross Williams Another thing I've been enjoying lately is literate programming. Amazingly it turns out to be faster to write a literate program than an ordinary program because debugging takes almost no time.
--Bill HartI've also found it useful when essentially plugging in data or HTML fragments into JavaScript. I can quickly write a trivial dsl that takes in the data in a convenient form and transforms it into a convenient code version. A variant of the transformational technique is when similar code is almost the same, but just needs a little subbing. I can write a single block covering most of the commonality, and then sub in the differences. For example, if writing some arithmetic operator function code, replacing '+' with the other operators.
An aspect I also love about the approach is having two views of the code. The LP view is more of an outline with blocks subbing in, with some transformations going on. The compiled LP version is one where you can see all the code in full context, minimizing the jumping around at that level, something that cannot be achieved with a bunch of functional calls.
Over the last few weeks, I have started to learn Elixir. I have noticed that I feel less drawn to use literate-programming for that language. It feels like it is so well designed that literate-programming is almost redundant for it. I am trying to figure out justifying that statement, but it is still early days for me in that language. But it feels like, for instance, the matching on function parameters so that one can avoid if-else if-...-else constructs cuts down on a lot of boilerplate stuff. Admittedly, JavaScript has a lot less of that as well nowadays with all the new language constructs. Maybe it is just about Elixir being a functional language with the safety and little overhead of calling functions that make it more attractive to use functions as the outlining / reordering mechanism. Also, the pipe operator allows steps of transformations to be done easily and clearly, which is super helpful. And having tests run from the doc code is something to embrace, further making the separate commenting in LP less favorable in the Elixir context.
One huge downside of LP is that it allows the dictates of a given language to be worked around, making it harder for others to follow up with the work. Ideally the text of the LP helps with that, but it still is a barrier. This seems less of a problem in the front-end web world because that is just a mess of competing notions, but in something cohesively designed with sensible standards that the community follows (my impression of Elixir), it would become a much bigger downside to strike out on your own path.
That said, though, I do still believe it and stand by it in all particulars.
We'll see how I feel in 5 more years..
# Chapter 1: The program
The main function is a simple infinite loop, repeatedly performing gather,
filter, and report tasks.
#+NAME: main
#+BEGIN_SRC c :tangle src/main.c :noweb yes
<<main_includes>>
int main() {
while(true) {
gather();
filter();
report();
}
}
#END_SRC
...
# Appendix A:
#+NAME: main_includes
#+BEGIN_SRC c
#include<gather.h>
#include<filter.h>
#include<report.h>
#+END_SRC
The reason to separate them is when the includes communicate nothing of great importance at a particular point in time. The structure of main, as trivial as it is, is probably more important to the reader than the includes. And since cross references and hyperlinks can be attached to the code block, it's straightforward to find where the noweb reference is. The same thing can be used to pull various other pieces out so that the presentation makes more sense (the output is meant to be read, so order becomes important in what and how you communicate).Using a sketch of a numerical simulation program, you may have this code structure:
-includes
-parse data
-simulation loop
-report result
For reasons the parsing happens first. But that may be the least interesting bit (the data is just be a straightforward series of lines each containing some number of space separated floating point numbers). Why present it earlier when the simulation loop is really the heart of the program? Or in the simulation, the complex bit may be some novel numerical algorithm at the core, and the rest is bog standard loops and data munging. Pull that algorithm to the front, and then insert it (using something like a noweb reference) in the body of the loop. In Foo22 a new algorithm is described, we have implemented it here:
.. code and prose
With this completed we can now write the simulation loop:
.. loop code people have seen 1000s of times before,
with a reference to the algorithm above