Epigrams on Programming - Alan J. Perlis
2020-07-02 12:33
This is a series of posts where I jot down my learnings from computer science papers I’ve read. Previously, A history of Clojure.
In A history of Clojure, Rickey cites Perlis’ Epigrams on Programming a number of times, specifically epigram 9:
It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.
I have not read this paper before, so I thought it would be interesting to dive in.
First off:
Epigram: a pithy saying or remark expressing an idea in a clever and amusing way.
Many of the epigrams are too clever for me to understand, and a lot of them are certainly amusing. I will highlight a few below, some that I have experienced, and some that I don’t fully understand and attempt to make sense of.
1. One man’s constant is another man’s variable.
This isn’t meant to be taken in the obvious manner, and perhaps refers to a person’s understanding of values/state in a program. It makes me think of Hyrum’s Law, in the sense that what you meant (constancy) is not how others use it (variability).
2. Functions delay binding: data structures induce binding. Moral: Structure data late in the programming process.
Reminds me of a Fred Brooks quote:
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.
There is a bit of symmetry here.
When designing, you want to start with functions, because functions leave space for growth. And good design leaves room for ambiguity and change. Later in the process, as you figure out more of what you are trying to build (through an iterative process, experiments, etc), you want more structure, hence data.
And conversely, when presented with tables (data structures), the logic becomes obvious, because there is less ambiguity. But with a flowchart (functions and program flows can be visualized with a control-flow graph), there is more room for interpretation.
I guess these two quotes are two ways of saying the same thing, one from the implementer point of view, and one from the reader point of view.
4. Every program is a part of some other program and rarely fits.
This is really interesting after reading A history of Clojure, because this is the motivation behind Clojure - building information systems using compatible, composable components.
5. If a program manipulates a large amount of data, it does so in a small number of ways.
Pareto principle, aka 80/20 rule. This pops up in a lot of places, such as features used, performance bottleneck, market distribution, etc.
6. Symmetry is a complexity reducing concept (co-routines include sub-routines); seek it everywhere.
The Wikipedia article on coroutines expands on this relation of symmetry:
The difference between calling another coroutine by means of “yielding” to it and simply calling another routine (which then, also, would return to the original point), is that the relationship between two coroutines which yield to each other is not that of caller-callee, but instead symmetric.
The coroutines referred to here are “symmetric coroutines”. Another kind of coroutines are “asymmetric coroutines”, which preserves a caller-callee relationship, in that when the callee yields, it is always to the caller. But execution of the asymmetric coroutine can still be suspended.
In Revisiting Coroutines, and in it the authors described symmetric and asymmetric coroutines, and argued in favor of asymmetric coroutines (emphasis mine):
Although equivalent in terms of expressiveness, symmetric and asymmetric coroutines are not equivalent with respect to ease of use. Handling and understanding the control flow of a program that employs even a moderate number of symmetric coroutines transferring control among themselves may require considerable effort from a programmer. On the other hand, since asymmetric coroutines always transfer control back to their invokers, control sequencing is much simpler to manage and understand. The composable behavior of asymmetric coroutines also provides support for concise implementations of several useful control behaviors, including generators, goal-oriented programming, and multitasking environments, …
So perhaps full symmetry might reduce complexity in terms of the concepts to consider, but increase complexity when trying to understand something.
7. It is easier to write an incorrect program than understand a correct one.
I face this day-to-day :). I strive to write code that is easy to understand, beyond just being correct1. I practice this by reviewing my own code after uploading, before requesting a review from my coworkers. This not only helps me to catch simple mistakes, such as typos (which can cause a round of back-and-forth), it also allows me to the chance to split up my change if it is too large or contains unrelated concepts. In my patches, I try to minimize reviewer effort, rather than time to merge the change, and I find that the former usually leads to the latter.
8. A programming language is low level when its programs require attention to the irrelevant.
Together with leaky abstractions, this means that high level languages do not exist :).
9. It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.
This is the epigram that Hickey cited (and tweaked) when discussing his decision to focus on the key abstraction in Clojure, seq
, and then later, when discussing transducers.
I’m not sure I agree with this idea. If you can break down your data structures into contained units, it becomes easier to understand. But perhaps at the expense of composability, because of the piping you would have to do in order to make these 10 data structures work together. If you only had one structure, you would have nothing to pipe.
This could also be a preference that depends on your familiarity with the system. When you are new to a system, having smaller data structures can help you keep things in your head and navigate the space. As you become more familiar and need to interact with more components, you prefer things to be lumped together, so that working with all of them is easy.
10. Get into a rut early: Do the same processes the same way.
Accumulate idioms. Standardize. The only difference(!) between Shakespeare and you was the size of his idiom list - not the size of his vocabulary.
I really like this epigram. I think the key point is that there should be one obvious way to do something. This is quite a broad statement, so let’s look at something specific: Makefiles.
Many projects describe how to build themselves using Makefiles. To build make build
, to test make test
. These common processes should be encapsulated, and be simple to do2.
Another example I can think of is package.json
used by Node projects. Many of these have convenient scripts such that when you run npm build
it runs a particular build script (regardless of what your build system is), and npm test
will run a test script (doesn’t matter what test runner you use).
11. If you have a procedure with 10 parameters, you probably missed some.
I chuckled. Working on V8, I am no stranger to functions that take many parameters (I would say the most I’ve seen is 8, I can probably find more if I look harder). This is a good reminder for myself, the next time I encounter such a case, I should probably take a step back and see how I can apply the Boy Scout Rule.
23. To understand a program you must become both the machine and the program.
This reminds me of the story of how Albert Einstein visualized himself running alongside a light beam. And how renowned rock climber Adam Ondra considers visualization a crucial part of climbing.
It also highlights the importance of good tooling to understanding how the machine is running your code, which will help with visualization.
Related: Make It Work Make It Right Make It Fast. But “Make It Work, Make It Right, Make It Easy To Understand, Make It Fast” doesn’t have the same ring to it.↩︎
The contents of those Makefiles will be a different topic, at least the commands exposed are relatively clean.↩︎