Custom Search

Wednesday, October 19, 2011

"More Power" is not always a good thing

In Succinctness is Power, Paul Graham ponders why Python — or any language — would choose readability over power. He isn't sure what is meant by regularity, and discusses several definitions of readability, but never gets to what I think is the core of the what Paul Prescod meant by his statement.

Readability isn't simply about the line or character counts that Paul talks about. It's about how quickly you can pick up code that someone else wrote, or that you wrote six months ago, understand it, and fix a bug or add a feature. Paul's discussion of programming features focuses on how easy it is to write code to do new things. If you're developing new code, that's what you want. On the other hand, if you're maintaining code, the hard part is usually figuring out what the original coder was up to, not making the required changes. In this latter case, readability is more important than power. In extreme cases, it's easier to rewrite the code from scratch than to figure out what it's doing. In fact, this is one of the rules Kernighan and Plauger's Elements of programming style: Don't fix bad code, rewrite it.

Readability also helps when the goal is to explain new things to other people. If you're planning on publishing the code to demonstrate or document a new algorithm of some kind, then readers need to be able to pick it up and understand it. A powerful feature that saves you 60 minutes of development time but costs thousands of readers a minute or two each to understand is a net loss.

Regularity is part of this. If a language is regular, then you can expect that if some language feature works for some type in the language, it'll work for all similar types; that all statements will be terminated in a similar manner; that function arguments will be evaluated in the same order everywhere; that — well, the list is potentially infinite. A language being regular improves readability by making it reasonable to infer the behavior of things you aren't familiar with based on the behavior of things you are familiar with, which saves you time when reading a program.

Paul argues that a language can't be too powerful, because the author can always write things out in the less powerful idiom if they don't like the more powerful one. The reader doesn't have that option. If the author is allowed to choose the more powerful option and does, then the reader has to read — and understand — the more powerful version.

Some people claim that a language is powerful if it has lots of features, so that whenever you need to do something, you have lots of ways to choose from for achieving the task at hand. This is in line with succinctness being power, in that if you get to choose between lots of ways to do something, you can chose the most succinct way. This form of power also leads to a less readable language. Some very popular languages have so many features that few, if any, people actually program in the entire langauge. Almost everybody programs in a private subset of that language — their dialect. So when you sit down to read a piece of code, you have to translate from the authors dialect to yours — which may require consulting the manual if it's sufficiently different from yours. In the worse case, you'll be reading code that was written by one person and then maintained by others, so you may well be switching between multiple dialects in reading a single piece of code — meaning that two apparently different bits of code may well provide identical functionality, but the dialect is different.

Examples

Let's look at some language features to see how power and readability trade off. We're going to look at one area of language design: passing arguments to a function. Functions are one of the most powerful features of modern languages. They allow code to be reused in multiple places. The arguments to the function allow the same code to be used with different data. Functions also improve readability. Once you read and understand a function, you don't have to do it again. If the author failed to use functions, but simply repeated the code each time, you'd have to read the code to verify that it's actually the same in each case. So functions are pretty clearly good for everyone involved. In particular, we're going to look at one task: figuring out how a variable in our source code gets the wrong value.

Let's start with the least powerful version of arguments, and one found in many modern languages. The language simply evaluates each argument, and passes the resulting value to the function. The function can't change the value of any variables named in the arguments. This is a very readable construct, as if you're trying to figure out what happened to a variable, you can ignore the function calls, because you know they didn't change the variable.

Now let's add a feature: let's let a function be able to change the value of a variable passed to it. This provides a more succinct way of using a function that changes the value of a variable. You can simply write:

def inc m
    m = m + 1

x = 3
inc x
inc x
inc x
print x

And it would print 6. If inc can't change the valaue of it's argument, then the equivalent code would have to be written something like so:

def inc m
   return m + 1

x = 3
x = inc x
x = inc x
x = inc x
print x

Each of the lines that comprise the function has twice as many symbols. More complex examples, involving changing more than one variable and possibly having a return value from the function as well, are much worse. So this new feature make the language more succinct, and hence more powerful.

From the readability standpoint, this is a disaster. Now you have to read every function that the variable you're interested in is passed to, because they may well change it. The amount of code you've got to examine has gone up by an indeterminate amount.

So if your language prefers readability to power, you'd want to give some serious thought to whether or not you want to add this feature to your language. If you do so, you will probably eventually realize that the problem is that there's nothing at the point the function is called to note that the variable could be changed. If you do things that way, then you only need to read functions that are passed variables flagged as changeable at the call point. The language has the same number of features as before, is only slightly less succinct, requiring one more symbol than the original version.

Now that we can change a variable passed to a function, let's look at the case where the variable of interest is passed to multiple function that are allowed to change it, and the values returned by those functions are passed to a function. That isn't inherently harder to read, until you ask the question What order are the arguments evaluated? If the language specifies an evaluation order, then you can read the functions that might change your variable in that order, and ignore the function those functions return values are passed to, unless it has your variable as an argument. If the language doesn't specify the order of evaluation, then you have to consider each possible calling order. But let's make our language really powerful! Let's let the called function decide not only what order the arguments are evaluated in, but allow it to evaluate them multiple times, including not at all if it decides that's appropriate. Lisp macros have this ability, and it's part of what makes Lisp programmers hate programming in languages without them. The invocation syntax is the same as functions, so for the purpose of reading code that uses them, they are the same as functions.

From a readability standpoint, this is a disaster of the same order as letting functions change the values of variables passed to them. It's not quite as bad, because it only affects functions which are called as part of evaluating arguments to other functions, which may or may not be all that common. But every time you examine such a case, you have to read through the outer function to figure out whether or not the function call you're actually interested in is evaluated. Worse yet, the answer may be maybe, as it will depend on the value of some variable in this new function. So now you're starting over with a different function and variable.

Conclusion

While these last examples are exaggerated, and real-world LISP programs are generally better written than that, the point is not to say that these features are bad, but to explain how more powerfull is not the same thing as more readable, and provide some motivation for preferring the latter to the former. I don't expect to change anyones mind about what programming features — and hence languages — they prefer. I hope I've helped someone understand why other people might prefer readability — and a language that values it.