Source Code Readability, Elegance, and Complexity
I was recently discussing my thoughts on this article:
And I thought I would speak more publicly on it.
I have spoken in the past about APL's reputation for "write only" code and argued that just because you don't know how to read a language doesn't make it unreadable. I think more and more people have begun to appreciate the fact that there is a difference between readability of code and one's comfort with that code. Just because some code makes you uncomfortable doesn't mean it is bad code.
The term simplicity and elegance are also big buzz words. We all want to make our code elegant and simple. But what does this mean? In general, elegance and simplicity is about reducing code complexity. We want to express things as directly and clearly as possible with the minimum necessary fluff (accidental complexity, cf. Ben Moseley).
But I think the discussion on source code readability, elegance, and complexity is missing a critical perspective. There is a nuance here that I think most people don't talk about, which is why it is something I try to emphasize in my own work. When people look at a piece of code that they don't understand that uses some new feature or some language that they aren't comfortable with and then conclude that the code is unreadable and that we shouldn't write code like that, they are not right, but they also aren't wrong.
To understand this, we should consider a more nuanced view of source code complexity. Let's consider some sources of code complexity, or complexity dimensions:
- The number of language features that are in use (language concepts, semantics)
- The number of libraries or vocabulary in use (leveraged domain knowledge)
- Design patterns and other idioms (structural habituation)
- The control flow of the program (branch complexity)
- The reference distance between definition and usage of the references (referential complexity)
- Spatial complexity of the code
- Syntactic complexity of the code
- Type complexity of the code
We can group these different complexities into two broad categories, of local and remote complexity.
|Remote Complexity||Local Complexity|
|Language Features||Control Flow|
I don't often see people discussing the debate about readability in these terms, if ever. However, in the debates, you will usually see people implicitly reference an intuitive conception that such a division needs to be made.
There are two common camps of readability and complexity. One tends to be those who want to leverage sophisticated language features or library design in order to help make programs easier to write and read. They will point out the beauty of the end result and how easy it is to divine what is meant or how short the code is or how good a job the compiler can do at generating good code for it. These are people that tend to feel an innate draw to the promise of DSLs (Domain-specific Languages) or who take advantage of sophisticated type systems. They will often also be those who make copious and sometimes even religious use of a large suite of libraries (often found in their favorite language's package repository), sometimes coupled with automated build systems to help put it all together. I tend to think of the Haskell or maybe C++ template-wizardry zealots as often following this camp.
The second popular camp are something of "feature minimalists". They often argue against the first camp for using too many complex features or making the shapes of their code too specialized to a task rather than following and using the more basic features of the language. They argue instead that the code is simpler and easier to work with when you have more regularity of use and the code is using the simple features that everyone will be able to read and understand. "I don't want to require a Ph.D. just to read my code," they might argue! They're Haskell preludes are simple (non-existant?). I often think of Go programmers or Standard ML users as being a part of this group.
These two camps are both arguing for more elegant, simpler code, but they will continue to fight and argue without making much progress with one another because they aren't actually talking about the real battle. They've missed the point somewhat. The focus in such arguments is almost invariably about the readability of the source code in discussion. But the real argument is one between remote vs. local complexity.
The first camp above is arguing for a large increase in remote complexity to reduce local complexity. The second camp argues for a massive reduction in remote complexity at the cost of local complexity.
The problem here is that neither is really correct, since it is asking the wrong question. Maybe another way of putting it is that the debate is moot. The focus tends to hone in on a single piece of code and debating whether or not that code is "elegant" or "readable." But I contend that this misses the reality, which is that code does not exist in a vacuum. It doesn't really matter if a single piece of code is elegant, it matters if code in general is holistically more elegant across the board.
I contend that people who are afraid of unreadability are really afraid of increased general complexity, but they might not know how to properly express it. They're wrong to say that such and such code is unreadable, but they are not necessarily wrong to be worried about such code. That is because it is possible to reduce local complexity down to a single point of utter simplicity that is maximally expressive if we are willing to arbitrarily increase remote complexity. If we take this to the extreme, this creates an infinite set of single token programs that are each locally maximally elegant and efficient, but which are completely useless to anyone, because they lack any connectedness or relationship between each other. This is like every single thought and set of thoughts and groups of thoughts in our heads all mapping to a single word for every single one of them, but where none of the thoughts can be described in terms of anything else. The result would be that we either know the word/idea or we don't, and if we don't, there's little hope of figuring it out.
The value in a program (or any definition of an idea) is its ability to connect one idea with another idea. In other words, it's the edges of the graph that matter, not just the nodes.
Of course, the opposite problem on the other extreme is that we reduce our expressiveness to the bare minimum necessary, at which point we have a fully powerful Turing machine, but which is incapable of being used well because we must be so explicit about everything that we do that we are unable to "subordinate detail" (Iverson's Turing Award Lecture) at any point in the programming process. We would be maximally precise and clear at all times and in all cases, but we would never be able to do anything great, because we would never be able to build hierarchies of concepts. The power of Naming isn't just for fantasy magic worlds in books (Rothfuss's Name of the Wind).
So what we have is a tension, and mathematically, it's an optimization problem. The thing we really care about optimizing is not the elegance of a single program, but the elegance in general of expressing the solution to any problem. The total complexity and cost of a program is in its remote and local complexity.
The important thing to note here is that remote complexity has an advantage that local complexity does not have. We can amortize remote complexity if that remote complexity can be applied to more than one problem. That's the power of remote complexity. But this isn't linear. It is perfectly possible to have poor amortization because the remote complexity is so specific to a set of programs (possibly even one program) that it becomes, in essence, a sort of pseudo-local complexity. But at the same time, if we don't have sufficient remote complexity, then the local complexity becomes too large.
So, if we properly introduce highly general forms of remote complexity, we are able to disproportionately reduce local complexity. Doing so reduces the overall complexity of programs in general, but this reaches a point of diminishing returns. At some point, the introduction of more remote complexity just shifts complexity from local to remote, without fundamentally reducing complexity. And this point might come sooner than you think, because moving complexity from local to remote is not free. It's fundamentally more expensive to rely on remote complexity for a program. If the complexity is local, it's easy to look at it and work with it immediately. If complexity is remote, you must either look it up, reference it, or have spent considerable time and effort into internalizing that complexity into a deep framework within your head (memorized it). All of this comes at a serious cost. Because remote complexity is *harder* to learn and use in the moment, you're actually increasing total complexity if you choose to move complexity that is too specific to remote complexity instead of localizing it.
Thus, readability and elegance is actually an outcome of a systems-level optimization of remote complexity to minimize total complexity within a sociocultural context of a programming community. In this ideal state, programs will be as elegant and as simple as they can be without making it more difficult to read programs in general. The sociocultural aspect is critical. Frankly put, we care about some people reading our code and writing code that we can read, and we don't care about others. There is a cultural and social context to readability, and while people may enter or leave this context, usually at will, we must always recognize that programs are written for certain people and not for others. The question of learnability ties in with readability, but that is for another time.
Programming languages are implicitly designed to favor a certain partition of remote and local complexity, but I have only ever read this concept of optimizing for global complexity reduction explicitly discussed in the language features advocated for in Ken Iverson's writings, which he called Economy. This is one of the reasons I find APL so fascinating. It is a language expressly designed around this concept of optimizing for global complexity reduction.