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
- 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.
Further thoughts
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.
View comments