Response to “Literate programming considered harmful”
johnwshipman.blogspot.com
johnwshipman.blogspot.com
I think this is related to the proliferation of frameworks. I can't come up with my own pattern that best fits the problem, but I can select from a list of pre-approved patterns in our framework and shoehorn the problem into it. We only hire people familiar with the framework, so no need to document.
It really comes down to laziness. A literate program demands you put forth the effort to understand it and read the docs and code. Whereas a with a framework program you can copy and paste code from blog posts and stack overflow without any clue what is really going on.
Generally only true for burger flipping software. If your problem is unique, there won't be a framework for it.
Want to be creative? Try seeking out creative problems.
Everything has exceptions, but this rule of thumb seems to apply well, in my experience.
That doesn't always stop the pressure to force-fit your problem into a "preferred" framework. In a large organization, both the people defining the problem and the people enforcing the constraints on the solution can be different from the people who have to implement the solution, and may not be aware of or able to understand the mis-match.
If you have some esoteric pattern in your code, then it's never worth the tradeoff between readability unless it is better performance wise.
Calling them the "lowest" common denominator patterns instead of best practices is being dishonest.
I have been at many sites where "esoteric" means things like first class functions (Python) or recursion. Not some hipster monad, y-combinator or method missing bullshit.
While the OP's tone is perhaps a bit harsh, I've been in his shoes before, where a team that doesn't understand why a problem doesn't fit their go-to solution forces you to write a convoluted mess to shoehorn the problem into the solution. performance isn't even the problem, usually. Maintainability is.
Enter Golang... a language smaller than even ANSI C. It's readable because if you've worked with Golang for more than a week, you basically know every language construct you'll encounter.
As an aside, I've found Clojure to be one of the most "readable" languages I've ever seen, in that I can usually just go to the source code if I'm trying to figure out how to use a library I just pulled in from clojars.
That's not really what readability is to me. For example, I consider this idiomatic golang code (from the standard library, no less) to be entirely unreadable:
https://golang.org/src/crypto/tls/handshake_messages.go#L297
For me readability is more about suitable levels of abstraction. Languages can affect that by making abstraction longwinded, or expensive at runtime, or hard to compose.
Really? It's a binary parser that's parsing data from a byte array and stuffing the results into a struct, it seems pretty straightforward.
If you've used Brainfuck for more than 5 minutes, you already know the entire language. Is it the most readable language ever designed?
> I can usually just go to the source code if I'm trying to figure out how to use a library I just pulled in from clojars.
Will a cursory read of the source code either tell you how the library deals with corner cases, or convince you that such corner cases don't exist?
version = ((data[0] & 0x0F) << 8 | data[1]);
But why? Is this still correct with the current specification of the message format? Who knows! And this is a best case, where "version" is a meaningful variable name.Oh, come on... BF was specifically/pathologically designed to be unreadable.
> Will a cursory read of the source code either tell you how the library deals with corner cases, or convince you that such corner cases don't exist?
Um... no, not really (at least not a cursory glance), but readability certainly helps track down issues later when you do run into an unforeseen corner case. Language choice is all about trade-offs -- if things like this are what is most important to you, perhaps you would be happier with something like Haskell or Ada.
I was just exposing a flaw in your “readability” criterion. Another example: you can learn the rules that govern a given cellular automaton in 5 minutes, yet be completely unable to predict what said automaton will do when run.
> readability certainly helps track down issues later when you do run into an unforeseen corner case.
I'd rather not have issues, and always be sure that I've handled everything. Only applied formal logic can help.
> Language choice is all about trade-offs
Normally, understandability trumps all else. The only exception is when it's too badly in conflict with performance.
> if things like this are what is most important to you, perhaps you would be happier with something like Haskell or Ada.
Nah. GHC Haskell and Ada are both ginormous languages that I don't believe anyone could fit into their heads in their entirety. In particular, with GHC Haskell, I don't feel confident saying “this code does exactly what I want and nothing else”, because someone might enable an extension I haven't accounted for, and then hilarity ensues.
(And nobody uses the language specified in the Haskell Report.)
One thing I found while messing around with Haskell is that while learning and writing vanilla Haskell is one thing, deciphering Haskell written by others is quite another, given the propensity of library authors to use language extensions and stuff like TemplateHaskell.
If you have any examples of code that, when an extension is enabled, neither fails to compile nor fails to behave in the same way it would be really interested to see them!
If I were to try to conjure some up the first place I would start would be interactions between scoped type variables and typeclasses.
Even if you tell yourself "oh, I'll use Observer", there are still so many details that can make your solution be boring and alright, or actually clean and magical. Foresight into how your structure will evolve, making things easier to test and read, separating the _right_ concerns.
I have copied stuff from Stack Overflow but always for solutions to a couple lines of code. I don't know what Stack Overflow answer I can copy for the general structure of my code...
In our consulting projects there is always a minimal level of documentation specified by the customer as delivery artifacts.
They want to be sure the next set of guys are able to carry on when there is budget available again.
And I'm not saying this because I've always had this view, I used to be too clever. I wrote a automatic caching and automatic windowing API client library. The thing was impossible for anyone else to alter because the mental model was too complex. Now I try to keep complexity only where it is needed and I try to keep the complexity quarantined from the interface as much as possible.
Huge projects like Rails can be clever while still making my life easier because they have an army of smart people finding the edge cases, but for most stuff that isn't the case.
Is that not a strawman argument?
# double the counts.
counts.map! { |count| count *= 2 }
Is the type of strawman I intended to avoid. using namespace std; // using namespace standardHasn't this always been the case for young programmers? Seems you don't comment until you have to go back to code you wrote yourself a year later and make major changes....you then realize you should have documented it, either inline step-by-step or as a proper doxygen/javadoc style.
I can thank Dr. Leitner (CS 50/51) for enforcing comments in assignments. Took programming courses as an undergrad (I wasn't a CS major, but a Bio/Int'l studies one) in C, no documentation required...code was checked for correctness only. Leitner had his TA's go through and take off points for code that wasn't documented...which needless to say got me in the habit of documenting.
>It really comes down to laziness.
Not so sure. Laziness or lack of good habits? Perhaps a mixture of both.
Or
- read the tests for that method.
- look at the commit log and associated bugs.
- use identifiers that are self-explanatory to some extent. auto-complete makes using long names manageable.
- step through it with a debugger to see what it does.
- use an editor/IDE that shows additional info about unknown methods, assuming a statically typed language
where it has total knowledge about available types.
Comments are certainly one way to understand code, but not the only way.Given a bizarre method, would you rather see a clear comment or spend half an hour stomping through a commit log?
In an ideal world, perhaps. But in the real world, the comments contradict the code, the code attempts to use a software pattern which doesn't quite fit (and thus requires a large number of hacks to make it fit), so reverse engineering is your only real choice.
Aiming for an ideal world is never a bad thing, but even if I saw well documented code, I wouldn't trust it. The only difference is that after reverse engineering it anyways, I might feel a bit of surprise when I find that the comments were, indeed, correct.
Not true. You just have to express it clearly. I view comments as a code smell - they generally indicate a part of the codebase that you know is poorly factored. If your language/framework doesn't allow you to express code clearly, get a better language/framework.
Laziness is one of the cardinal virtues of a programmer.
Comments that explain what the code is doing are usually a travesty. With the possible exception of hairy performance optimizations. Also implementations of algorithms that aren't well known -- oftentimes the original paper is dense, academic and behind a paywall, and the Wikipedia article are a muddled mess. You really owe it to your readers to give a sane and comprehensible explanation instead.
Comments that explain why the code is doing what it does, though, are HUGELY important. There's nothing worse than coming into a legacy codebase and seeing it littered with obvious special cases and workarounds that are all undocumented, so that you have no idea what they're for or whether they're obsolete or not. Skipping these kinds of comments leads to inexorable technical debt, because eventually you'll end up with a codebase where everyone is afraid to change the code.
It seems literate programming assumes that the person doing development is the same as the one reading documentation. That is not always the case. And when it is not the case, the code embedded in documentation can hurt readability.
For example, I try to get all interface specs distributed, discussed and confirmed ASAP, usually WAY before the code is mature enough to be shown outside. Also, getting others to read, think about and comment on even short ICDs is difficult as is. Getting them to read early ICDs interspersed with early code? Not going to fly. My 2c.
* This headline won't export :noexport:
These are some comments for the user and maintainers
of this document. Describing our formatting and
practices.
* This headline will export
And here's the text describing some message format.
And here's a table showing the layout:
| Word | 12-15 | 8-11 | 4-7 | 0-3 |
| 0 | WordCnt | TimeTag| ... | ... |
And here's the source for this as a C struct, but
it's a work in progress. So we won't export it yet.
#+NAME: MSGXXX_struct
#+BEGIN_SRC c :exports none
typedef struct {
...
} MSGXXX_t
#+END_SRC
No one has to see that stuff until I'm done with it. Or, other tags could be used so I can explicitly select what gets exported depending on the audience. The customer doesn't need the code (at this point). But fellow devs (local, or at partner organizations) may.In my (maybe skewed) experience though once 2 or more people start adding comments on the same place of code things can get very confusing quickly -- with mishmash of different styles, commented out comments, multiple copy-paste, etc., so the only way to understand what is going on is to delete the whole Kunstkamera and look only at the code.
You can get code only views, no deletion necessary. Run tangle, which will spit out all the source files (which is part of the compilation/execution step anyways), or use (org-mode specific): C-c C-v C-v (org-babel-expand-src-block). The latter gives you the expanded (this assumes you're using something like noweb[0]) view of the code in a read-only buffer.
Org uses optional noweb notation to include blocks of code in other blocks:
#+NAME: print_hello_world
#+BEGIN_SRC c
printf("Hello, World!\n");
#+END_SRC
#+BEGIN_SRC c :noweb yes :tangle hello.c
#include<stdio.h>
int main(int argc, char** argv) {
<<print_hello_world>>
return 0;
}
#+END_SRC
<<name>> will be substituted with the contents of the other block. The expand-src-block will give you a view with all substitutions made. C-c C-v C-t (org-babel-tangle) will generate hello.c (could be prefixed with a directory, my preferred way, so it'd be "src/hello.c").A more useful case than the above, I have a switch/case statement. Each case gets broken into its own source block with documentation around it like:
* Handling different message types
#+BEGIN_SRC c :noweb yes
switch(msg.type) {
<<msg_101>>
<<msg_102>>
<<default>>
}
#+END_SRC
** Message 101
Documentation
#+NAME: msg_101
#+BEGIN_SRC c
case 101:
// handle 101
break;
#+END_SRC
** Message 102
Documentation
#+NAME: msg_102
#+BEGIN_SRC c
case 102:
// handle 102
break;
#+END_SRC
** Default case
Documentation
#+NAME: default
#+BEGIN_SRC c
default:
// handle default
#+END_SRC> the ends are insufficiently ambitious by focusing on a passive representation [0]
After practicing literate programming seriously for over a year on a large project, I believe that our notion of documents themselves is insufficiently ambitious, for the same reason.
Computer-based media should not be limited to the static forms imposed by paper. It's unfair to critique literate programming per se until we have a truly dynamic mode of representation. (edit, yes, technically we have that now, but it's an order of magnitude more work to create. I'm saying that live, dynamic representations should be our tool for thinking, building, and writing about things in the first place, not after the fact.)
This was for a project called "willshake." The system provides a rough overview of itself [0].
I was using Org for documentation, and I decided to try migrating to a literate codebase.
It was clear immediately that I needed a "real" build tool. After some research, I decided that was Tup [1]. Tup is a strict, "functional reactive" build system with extremely fast incremental builds.
So I run the Tup monitor, and everything gets tangled as I write. Great.
But tangling through Emacs/Org was way too slow. So I wrote my own gawk-based tangle [2] that supports the subset of Org Babel's features that I needed, but would tangle any file in 20-50ms.
But usually I want to do something with the tangled code (you know, compile it, run it). So I also define build rules inside of the documents (for example, [3]). I just extract the rules from the documents and feed them to Tup.
Now I can just write whatever I want in a document, and stuff just happens as I write. This includes the removal of obsoleted build targets, which Tup handles. So I can "literally" remove a document file from the project and the feature is removed; return it, and the feature is restored.
But as the system grew, the build started to push 2 seconds. This is because Tup was reviewing thousands of rules whenever any file changed. So I decided (following Christopher Alexander), to treat directories of documents as isolated subsystems.[4]
So I can create features and subsystems with continuous builds ~250ms, using only Org files and directories.
It still sucks. I'm taking a detour to explore dynamic documents, which I think precedes considerations about "programming."
[0] https://willshake.net/about#more-about
[2] https://bitbucket.org/gavinpc/willshake/src/default/bootstra...
[3] https://willshake.net/about/the_stylesheets#sec-4
[4] https://bitbucket.org/gavinpc/willshake/src/default/bootstra...
EDIT: Thanks for the tip about tup. That looks like something I could get into. Been reading about it some this afternoon, I'll check it out more tonight or this weekend.
Thanks for the links and the resources. My target with literate programming is a bit different than yours, but you've given me some things to think about. I appreciate that.
You can also read Knuths description of the Web system he under to write the code here: http://www.literateprogramming.com/knuthweb.pdf
For that reason, I find myself constantly reverse engineering code. If I am tracking down a bug and run across a bit of obfuscated code which has the comment "optimized hotspot sorting", I'm going to assume that it's wrong. I'm going to take the time to reverse engineer that obfuscated code, to make sure that the code is actually sorting, that it it an actual hotspot, and that it's not the cause of the bug I'm chasing down.
I guess I use it as a negative signal: "the comment says the code does X, I need to verify that it actually does X, correctly". "Optimized" code blocks and comments taken as the truth have pointed me at more bad code than good code.
- Make the program in complete slices of a functionality and put those into folders and files that are descriptive of what the code does: "web/login/facebook/always_ask_for_email.lang"
- Do repeat yourself in different functionality areas.
- Name your variables and functions as if they should never conflict/as specifically as you dare.
- Use a functional and immutable programming language or write pure functions as much as possible.
- Stop thinking that patterns are necessary to solve the problem of turning data from one form into another.
I might be wrong.
When project wasn't that researchy in the first place (does not require significant experimentation to come up with the solution), or exits the research phase I move to standard programming tools.
I like stuff that's well documented and I find my own documentation crucial for understanding things.
I am however a bit at a loss about the practicalities.
Does writing a doc-string for each class/method/function and connecting to readthedocs count as literate programming?
I don't think so. It's like comparing the Lord of the Rings, the novel, with the index that contains descriptions of all the characters in the Middle Earth.
I mean both tell you the same story, but from a very different perspective.
I am personally not a fan of (what I understand as) literate programming, unless you are writing a piece of educational code that is supposed to be read from beginning to end (like Jones Forth, for example).
What I think should be done instead, the code should be documented on several levels, each level more detailed by a constant factor. So each couple lines should have a comment, each function should have a comment what it does with what parameters, each class (or group of functions) should have a description of their purpose, each module should have a description of its architecture and interactions, and so on until you would get to the top level architecture. So it would be more like a tree of more and more detailed documentation rather than linear novel that you read from start to end.
In my relatively limited experience of literate programming, it adds unique value in two ways:
1. By embedding code in a narrative which, when lucidly written, yields context and understanding of what it does (and doesn't do) and why, which is far superior to what a later developer could derive purely from analysis of the code and whatever other artifacts exist around it;
2. By using capabilities like hyperlinks, folding, and TOC/outline generation to support much more fluent navigation and autotitration of complexity than are possible without.
Learning and using tools which support these capabilities is undoubtedly more work than not doing so. Especially in a fast-paced industry environment, doing so is often both hard to justify, and just plain hard - at least in my experience, to do it well you need to be not just a good programmer but a good writer besides, and it is very hard to be that latter thing without the benefit of time to reflect and to edit. But my, again limited, experience suggests that the effort when possible can pay off very effectively.
Literate Programming is more about how everything fits together, in /one/ coherent explanation.
For example, the doc string of "search" tells you the returned list is sorted afterwards. It should maybe tell you the complexity of its algorithm like O(n log(n)). It should NOT state which algorithm (QuickSort, TimSort, etc) it uses. Comments in the source code can explain the choice of algorithm.
What question is that?! If you write malware with it, yes it is! Though not for you.
Making it a question just gives the extra benefit of making it likely to conform to Betteridge's law. So a meme win-win really.