On the Python-ideas list, in yet another thread on a way to embed statements in expressions, I raised the issue that the statement-expression distinction, and not having a way to escape it, is important to why Python is so readable. But I couldn't explain exactly why.

The question ultimately comes down to: why have statements in the first place? After all, there's no reason you can't make (almost) everything an expression, even in an imperative language (Ruby and CoffeeScript do so), and use significant indentation within expressions (again, CoffeeScript does so).

Guido's answer

Hm... Practically every language I knew before I designed Python had this distinction built right into the grammar and other assumptions: Algol-60, Fortran, Pascal, C, ABC. Even Basic. I was aware of the alternative design choice: Algol-68 had statements-as-expression, and Lisp of course -- but I wasn't a big Lisp fan, and in Algol-68 it was largely a curiosity for people who wanted to write extra-terse code (also, IIRC the prevailing custom was to stick to a more conservative coding style which was derived from Algol-60). 
So it's hard to say to what extent this was a conscious choice and to what extent it was just tradition. But there's nothing necessarily wrong with tradition (up to a point). I think it still makes sense that statements are laid out vertically while expressions are laid out horizontally. Come to think of it, mathematics uses a similar convention -- a formula is laid out (primarily) horizontally, while a sequence of formulas (like a proof or a set of axioms) is laid out vertically. 
I think several Zen items apply: readability counts, and flat is better than nested. There is a lot to be said for the readability that is the result of the constraints of the blackboard or the page. (And this reminds me of how infuriating it is to me when this is violated -- e.g. 2up text in a PDF that's too tall to fit on the screen vertically, or when a dumb editor breaks lines but doesn't preserve indentation.)
Guido's analogy with mathematical proofs is compelling. It's a large part of the design philosophy of Python that code should read like English when possible (e.g., the use of the colon is based on the way colons are used in English sentences), or like mathematics when English doesn't make sense (e.g., operator precedence).

And ultimately, "readability counts" is the answer here. But hopefully there's a way to explain how statements help readability, that gets to the actual fact behind Guido's analogy, and behind the intuition behind the tradition.

Ron Adam's followup

Expressions evaluate in unique name spaces, while statements generally do
not. Consider "a + b"; it is evaluated in a private method after the values
a and b are passed to it. 
Statements are used to mutate the current name space, while expressions
generally do not. 
Statements can alter control flow, while expressions generally do not. 
Having a clear distinction between expressions and statements makes reading
and understanding code much easier.

I'm not as sure about Ron's first three points. For example, in Python, "a[0] = 2" is evaluated by calling an a.__setitem__ method with the values a, 0, and 2 passed to it, exactly as "a + b" is evaluated by calling an a.__add__ method with the values a and b passed to it. And C++ takes this farther, where even normal assignment is a method call, and Ruby takes it even farther, where a for loop is evaluated by passing the object being looped over and the proc to call on each element to a method. So, that only requires a handful of things to be statements, like break, continue, and return (the very things that are statements in Ruby and CoffeeScript).

But his last point, that's the whole crux of the matter: I think having a clear distinction does make reading and understanding code easier, and it's a large part of why idiomatic Python code is more readable than Ruby or CoffeeScript code. (Guido raised another point earlier about CoffeeScript: its grammar is basically not defined at all, except in terms of translation rules to JavaScript, which means it's impossible to hold the syntax in your head. And I agree—but not many other languages suffer from that problem, and yet they're less readable than Python.) The question is why it makes reading and understanding code easier.

My thoughts

Actually, I don't think having the distinction does necessarily make reading code easier.  It enables a language to make reading code easier, but depending on how it makes other choices, it may not get that benefit. After all, JavaScript is less readable than CoffeeScript, and I think Java and C++ would both gain from being able to do more in expressions, but Python wouldn't.

What's the difference? Partly the fact that compound statements are indentation-driven rather than brace-driven, but standard style guidelines for other languages (which the vast majority of code follows) already recommend that. And partly the fact that terse forms (like comprehensions) are only for purely declarative code, but those other languages don't have any corresponding forms in the first place.

The big difference is that mutating functions (idiomatically) don't return the mutated object. This means that fluent style code is impossible to write in Python, which comes at a cost. More generally, Python is not a great language for writing some kinds of complex expressions that other languages can handle easily. But it also provides a benefit. When combined with the two preceding features, and the statement-expression divide (and a well-designed language and set of idioms) you get two things that are nearly unique to Python:
  • Flow control is immediately visible by scanning the code, because it's represented by indentation levels and very little else is.
  • State mutation is almost immediately visible by scanning the code, because each line of code usually represents exactly one mutation, and usually to the thing on the left.
That's a lot of important information to be able to pick up without thoroughly reading the code and thinking through it. It often lets you quickly find the part that you do need to read thoroughly and think about, just by skimming the rest.

This implies that there might be different ways to make languages readable than the way Guido came up with. And to some extent, I think Ruby and CoffeeScript are both examples of steps in one possible direction. But, even though they've broken with tradition more than Python has, they haven't gotten as far yet. The real question is whether you can find a way to make functional-style code as nice as it is in Ruby (or even ML or Haskell) while making imperative/OO-style code as nice as it is in Python. I think that would require some big clever new idea nobody's come up with yet, not just tweaking the basic designs of Ruby and Python, but maybe I'm wrong.

Further thoughts

Earlier in the thread, Franklin Lee pointed to a great blog post by Guido, Language Design is Not Just Solving Puzzles. In that post, Guido's main point is that attempting to "solve the puzzle" of how to embed statements into expressions in Python is wrong-headed. And he's got an interesting argument.

I had suggested that the inability to escape the division was important for reasons beyond just keeping the parser simpler, but Guido suggests that keeping the parser simpler is already enough reason. Anything that's complicated for the compiler to parse is also likely to be too complicated for a human to parse instinctively; if you have to stop and think about the syntax, it gets in the way of you reading the code.

C and its derivatives provide a perfect example of what he means. The declaration syntax is complicated enough that there are interview questions and online puzzles asking you to parse your way through this and explain what the type is:*
    const char **(*)(const char *(*)(const char *), const char **)
Or, in C++, to explain why this function doesn't construct a list named lines:**
    list<string> lines(istream_iterator<string>(cin), istream_iterator<string>());
Anyway, Guido suggests that parsing Python is essentially modal: there's an indent-sensitive statement-reading mode, and a non-indent-sensitive expression-reading mode. You can embed expressions in statements, but not the other way around, so you basically just need a mode flag rather than a stack of modes. And it's not that the stack would be too complicated to code into a parser, it's that it would be too complicated to fit into a reader's intuition. Anything extra you have to keep in mind while reading is something that gets in the way of your reading. (He puts it better than me, and not just because he devotes a whole post to it rather than just a couple of paragraphs, so go read it.)

Elsewhere in the thread, Guido also raised the point that being able to hold the syntax in your head is important, pointing out that part of the problem with understanding CoffeeScript is the fact that you can't possibly understand the syntax because it isn't even defined anywhere except as a set of translation rules to JavaScript.

I think he's right about both these points. And if you combine them with the fact that statements can be (and, in Python, are) used to make code more skimmable, that explains why being able to embed a statement in an expression is probably a losing proposition.


* It's a pointer to a string-specific version of the map function. C represents strings as const char *, so const char ** is an array of strings. The parentheses around the next asterisk are necessary to make it part of the whole declaration rather than part of the return type, so we're declaring a pointer to a function that returns an array of strings. The first parameter type is similarly a pointer to a function returning a string, and taking a string, and the second parameter type is an array of strings. It may be easier to read in SML syntax (or, really, almost any other syntax…): (string -> string) * (string list) -> string list.

** In C, some constructions can be parsed as either an expression or a declaration, both of which are valid statements; C resolves this by treating any such ambiguous constructions as declarations. That doesn't come up too often in C, but in C++, it does all the time. Here, we're trying to construct a list<string> by calling the list constructor with a pair of begin and end iterators. For the begin, we're passing istream_iterator<string>(cin), which iterates lines off standard input. For the end, we're passing istream_iterator<string>(), which is the default end iterator of the same type. But istream_iterator<string>() can also be read as either a type—a function that takes nothing and returns an iterator—or an expression—a call of the constructor. So now we have to see whether the rest of the statement can be read as a declaration to know which one it is. istream_iterator<string>(cin) can be read as a type followed by an identifier (in unnecessary, but legal, parentheses). Which means the whole thing can be read as a function declaration: lines takes an iterator named cin, and a function that returns an iterator with no parameter name, and returns a list.
3

View comments

Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.