A Process for Readable Code

I took a course on data structures and algorithms over the last few months. It is being offered as a part of IIT Madras’ Online Degree Program in Data Science and Programming, taught by Prof Madhavan Mukund. The program is a MOOC in a true sense, with tens of thousands of students enrolling each year. The DSA course itself is offered every trimester, and sees an average of ~700 enrollments every time. It is easy to see how communication becomes critical in running a MOOC at this scale. There is, of course, the operational and logistical communication that goes into the smooth running of the course. But more relevant is communicating the content of the course (the course is also highly interactive - in addition to weekly office hours, there are Discourse forums where students, TAs and faculty are active).

Communicating anything about programming is no joke, especially when the programming experience of students varies widely from novices to ninjas. Experienced developers may have perfected the art of communicating with commit messages, or via group chats. However, I’m not aware of any widely adopted protocol or best practices which help people teach and learn coding better. The StackExchange model comes very close, but it’s not particularly well suited to pedagogy.

Let us assume that there are broadly to aspects of programming when it comes to teaching programming - ideas and code. So, how do

  1. teachers communicate programming ideas to students?
  2. teachers communicate nuances of code to students?
  3. students communicate on either aspect with each other, (especially when asking for help)?
  4. students communicate with the teachers?

I clearly don’t have specific answers to these, but I can certainly think of many things which could help.

Communication as Code

One of the pleasures of listening to Madhavan Mukund is that his speech is like other people’s writing. Writing is easy. In the middle of writing a sentence, I can decide that I could have written the beginning better. Then I can always go back, change the beginning and rephrase the sentence as many times as I like. Speaking is difficult. If I realize that I messed up my sentence in the middle of speaking it, I will most certainly fumble. In his lectures, Madhavan says exactly what he intends to say. This ability can only come from a place of the deepest expertise and clarity (I recently found out that he does do retakes. A part of me died that day - I won’t lie. Even so, knowing how much editing should take place in a recording so as to make a perfectly effective point is a rare skill). The lectures typically contained Python code and discussion on them, but the code eventually got a bit too verbose and ultimately started looking a lot less Pythonic. The course could really use code that was a lot more readable.

But do I know this for sure? I, as an experienced Python developer, have a certain opinion on the merits of readable code. Do these supposed merits extend to teaching data structures and algorithms? Eventually, I stopped reading too much into the on-screen code, and instead started listening to what Madhavan was saying in English. This I translated to pseudocode, and then to Python. And what he was saying in English was so clear and precise, that it might well have been the pseudocode. This strategy worked well for me - even though I got an embarrassing B+ in the course, if I had resorted to simply reading the on-screen code and deciphering what it was doing, I’d have failed the course for sure. The on-screen code was way more verbose than Madhavan’s explanations (of course, all code is by definition more verbose than its pseudocode, but not unnecessarily so). Listening to explanations, following it up with your own implementation, and then trying to reconcile it with the on-screen code was very counterproductive.

Metrics for the readability of natural language (like the Flesch-Kinkaid tests for English) are well understood and widely adopted. I don’t know of similar metrics for the readability of code. There has been some work around designing such a metric - but eventually all of it boils down to the subjective opinion of relatively few people (English readability, too, is subjective, but a lot more people speak English than write code). And that’s how it should be, too, but the fact remains that there isn’t one comprehensive, widely accepted measure of code readability. The good news is that just like in natural language, writing readable code can be taught and learnt (Uncle Bob wrote and entire book on this, after all).

While code readability itself may not be easily measurable, what we need to do in order to write readable code is well accepted. We all know to use meaningful variable names, short and well documented functions, classes that are not too deep in the inheritance tree (or worse, an inheritance graph), etc. In most cases, it seems like readable code comes from making code as close to natural language as possible. So now we can ask a more focused question: We know that readable code serves experienced developers well - does it also serve students equally well? Or would they rather study a piece of Python code that is so verbose, so much like C, that GCC could actually compile it with little modification?

This distinction between how experts and novices perceive readable code is critical. Primarily, because it is prima-facie difficult to “sell” readable code to novices. Consider this for instance: how would you convince a student that a list comprehension is better than even the simplest for loop? The latter isn’t technically wrong at all. In fact, it can be argued that unlike list comprehensions, for loops are universal and therefore should be preferred - they are instantly recognizable to anyone. On the other hand, list comprehensions are idiomatic only in some languages. List comprehensions over for loops is not an easy pitch.

I asked a few senior developers whether they would sell the merits of clean code to their mentees, and if so, how? Everyone agreed that it was a difficult problem. There are no jobs in clean / readable code as such. It does not even have the satisfaction of writing code that “just works”. So how do people inculcate these habits among their mentees? Some of them don’t care, some of them enforce it and others chisel away at the habits of their mentees until it sticks. A friend also reminded me that great programmers can come from all three categories of mentees. This was a wake up call. Readable code has a lot more to do with long-term benefits for a team or a community than with short-term individual ability.

This is precisely the thesis of Donald Knuth’s philosophy of literate programming:

Let us change our traditional attitude to the construction of programs: Instead of imagining that our main task is to instruct a computer what to do, let us concentrate rather on explaining to human beings what we want a computer to do.

If teaching programming isn’t about explaining to human beings what we want a computer to do, what is it about? Can we talk about a programme so clearly in English that the actual code becomes secondary? This is precisely what led to a lot of my learning during the DSA course. Here I come full circle to my argument about code and natural language - a readable version of a program is one that can most easily be translated to natural language. If a human can read a piece of code and narrate it to others, we can call it readable (assuming, of course, that the speaker is a good communicator and the listeners have the requisite amount of programming skills).

Examples: Improving Readability

I took the implementations of three relatively straightforward algorithms - mergesort, breadth-first search and Dijkstra’s algorithm - from the course material and tried to make them more readable, while recording my attempts. I thus ended up with the “before” and “after” versions for each algorithm. I then asked a bunch of people which version they found more readable, and here are the results.

1. Mergesort

37 people saw the before and after versions of the code, and 27 of them (~73%) of them found the “after” version of the code more readable.

(Interestingly, some of my friends also attributed the readability of the code to factors external to the code itself - like font sizes, colors and how an IDE might render whitespaces. Sure, these external factors are largely based on peoples’ taste and preferences, but it is still worthwhile optimizing one’s developer environment for better optical readability.)

Again, 37 people saw this, and 28 of them (~75%) preferred the “after” version. (At lease one of the 9 who favoured the “before” version did so because they did not know what a Python deque is - I wonder how many others were put off by that).

3. Dijkstra’s Shortest Path Algorithm

27 people saw this and 23 of them (~85%) agreed that the “after” version was more readable.

Distilling the Process

In an attempt to carve a process out of what I’ve done in the videos above, I have slotted everything I have done in the following categories:

0. Perfect Information

This is an obvious prerequisite. Without knowing the problem (and the solution) well enough at the lowest level of abstraction, attempting to write readable code is tantamount to premature optimization.

1. Decide on the Level of Abstraction

In the first place, isolating where your problem lies within an entire stack of insanely complex logic is very difficult. To add to that, when a problem spans multiple levels of abstraction, lack of readability can severely hurt problem solving. When dealing with such variety in abstraction, it is often a good idea to abstract away the lowest levels of code, making them as simple as they can be. This typically boils down to deciding whether to write a new function or a class. In the extreme case you might find yourself writing a function that is being used exactly once in an entire application. But code reuse is only one aspect of a function. Other major aspects are logical isolation and repeatability. In his talk on Developer UX, Francois Chollet stresses the importance of reducing of cognitive load on users. That is precisely what good abstraction does, and indeed the best API design often emerges from such abstractions. Unfortunately, we typically see clean and simple API design as primarily user-facing. We should extend the same empathy to our fellow developer. After all, every layer of abstraction is someone’s API. It’s turtles all the way down.

2. Clear parameter and variable names

naming variables is hard

This, too, is easier said than done. But with enough practice (or after being bitten enough times by your own unreadable code), good variable naming becomes a habit. If you stick to it, you will soon reach a stage where you will not be able to stomach iterator variables named as i, j, k. It is not that single-letter names don’t have their place, but words, phrases and abbreviations are a lot more sticky. A well chosen name acts as an anchor point. You could come back to a piece of code years later, and reconstruct your memory of the program with nothing more than a single well named object.

3. Idiomatic Programming - know your language

Here’s the most useful piece of advice written in the entire official documentation for Python - a little note under the Library Reference section says

keep this under your pillow

Idiomatic programming rests on knowing which features a language has, and utilizing them to the greatest extent. For instance, swapping the values of variables in Python can be done simply with tuple unpacking - as against using a temporary variable, which would be un-Pythonic. Almost every use of idiomatic code can be associated with reduced verbosity.

3.1. Know the standard library

On the first day of my very first job, I was told that I’d be expected to know the Python standard library very well. A decade later, I’m still discovering modules in it. A colleague of mine once wrote a JSON parser in Python. Someone told him that he could just do import json; json.loads(...). He didn’t speak to anyone for two days. (In his defence, he was a competitive programmer.)

I doubt if it is possible to find someone who knows the standard library of any language, let alone Python, like the back of their hand. But the good news is that the offerings of a standard library are discoverable. However, even these opportunities at discovery can be easily missed. I would definitely use Google if I don’t know the solution to a problem. I probably won’t use Google if I don’t know that my solution is sub-optimal. In the Dijkstra example above, I managed to save a few lines of code because I simply happened to know that math.inf exists. This was superior knowledge, not superior skill. Regular code reviews are a good way to improve upon this lack of knowledge.

3.2. Know common data structures

“Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”

- Linus Torvalds

Choosing the right data structure for a problem has more to do with performance than with elegance or readability. For example, it is clear that types like sets and dictionaries, which are optimized for search are better for checking memberships of elements instead of simpler collections, like lists. But once a set has been chosen over a list, code must be rewritten for the better usage of its methods and optimization of its costs.

For a “batteries included” language like Python, data structures tend to be highly synergistic - which means that often, explicit typecasting can be avoided. Instead, there will almost always be a method that returns a view of the object that you really need. Consider, for example, two dictionaries, X and Y. Suppose you have to do something with the keys present in exactly one of them. Typically, to find such keys, I would simply iterate.

ind_keys = [key for key in X if key not in Y]

But I did not know until recently, that a collection of dictionary keys in Python behaves a lot like a set! So the symmetric difference operator (^) that we generally use on a pair sets can also be used on a pair of dict_keys objects:

ind_keys = X.keys() ^ Y.keys()

Keeping up with this knowledge is tough for even the most experienced developers - especially as newer versions of libraries and languages come up. Here, too, regular code reviews (preferably from multiple reviewers with varying levels of expertise) are immensely useful.

These steps need to be rinsed and repeated, since each of them affects the others. For instance, it’s likely that you discover a data structure that simplifies (or even eliminates the need for) a function, ultimately hoisting the program to a higher level of abstraction. So, a single pass through these steps alone may not suffice. And, as usual, it always helps to have an experienced programmer review your code. As long as your reviewer doesn’t feel like they are solving a Holmesian mystery, you’re good.

comments powered by Disqus