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

  1. As I read more about this debate, I'm starting to get the sense that in Guido's mind, part of buying into the philosophy of Python is accepting the fact that Python won't support some advanced things because we want to keep the internals simple and easy to hack on/extend. I can accept that idea; e.g. CPython is famous for being able to interface easily with C.

    ReplyDelete
    Replies
    1. Well, yes. Once you accept that almost every language design issue comes with tradeoffs, keeping the implementation simple enough to hold the semantics in your head and to hack on the interpreter has to mean giving up some other things. But I don't think that's motivated Python's design to the same extent as, say, Scheme's. Many things that you probably think of as desirable "advanced features"--including statements inside expressions, and tail call elimination, and explicitly creating multiple GILs in one process, and a more complicated (Swift-like) iterator protocol, and so on--are things he explicitly doesn't want in the language, even if they _would_ be easy to implement. For example, he's talked about how TCE would encourage people to write recursive solutions for inherently iterative problems, which violates TOOWTDI and usually leads to less readable code, so he doesn't want it even if it would be a simple change (and it would be--adding an explicit tail call syntax and a bytecode that does it is easy; for implementation it's almost just a matter of replacing a recursive call in the eval loop with a continue; making the peephole optimizer detect most cases of implicit tail recursion is trivial). So, occasionally some naturally-recursive code that may need a depth > 1000 has to be rewritten with an explicit stack, but if you look at actual Python code (as opposed to examples written by Lisp users to complain about Python) that's very rare, and the cost of the attractive nuisance outweighs the benefit of being able to simplify that rare code.

      Delete
  2. Real instructive and excellent body structure of subject matter, now that’s user pleasant (:
    Python print statement | Python multiline comment in Python 3

    ReplyDelete
Hybrid Programming
Hybrid Programming
5
Greenlets vs. explicit coroutines
Greenlets vs. explicit coroutines
6
ABCs: What are they good for?
ABCs: What are they good for?
1
A standard assembly format for Python bytecode
A standard assembly format for Python bytecode
6
Unified call syntax
Unified call syntax
8
Why heapq isn't a type
Why heapq isn't a type
1
Unpacked Bytecode
Unpacked Bytecode
3
Everything is dynamic
Everything is dynamic
1
Wordcode
Wordcode
1
For-each loops should define a new variable
For-each loops should define a new variable
4
Views instead of iterators
Views instead of iterators
2
How lookup _could_ work
How lookup _could_ work
2
How lookup works
How lookup works
7
How functions work
How functions work
2
Why you can't have exact decimal math
Why you can't have exact decimal math
2
Can you customize method resolution order?
Can you customize method resolution order?
1
Prototype inheritance is inheritance
Prototype inheritance is inheritance
1
Pattern matching again
Pattern matching again
The best collections library design?
The best collections library design?
1
Leaks into the Enclosing Scope
Leaks into the Enclosing Scope
2
Iterable Terminology
Iterable Terminology
8
Creating a new sequence type is easy
Creating a new sequence type is easy
2
Going faster with NumPy
Going faster with NumPy
2
Why isn't asyncio too slow?
Why isn't asyncio too slow?
Hacking Python without hacking Python
Hacking Python without hacking Python
1
How to detect a valid integer literal
How to detect a valid integer literal
2
Operator sectioning for Python
Operator sectioning for Python
1
If you don't like exceptions, you don't like Python
If you don't like exceptions, you don't like Python
2
Spam, spam, spam, gouda, spam, and tulips
Spam, spam, spam, gouda, spam, and tulips
And now for something completely stupid…
And now for something completely stupid…
How not to overuse lambda
How not to overuse lambda
1
Why following idioms matters
Why following idioms matters
1
Cloning generators
Cloning generators
5
What belongs in the stdlib?
What belongs in the stdlib?
3
Augmented Assignments (a += b)
Augmented Assignments (a += b)
11
Statements and Expressions
Statements and Expressions
3
An Abbreviated Table of binary64 Values
An Abbreviated Table of binary64 Values
1
IEEE Floats and Python
IEEE Floats and Python
Subtyping and Ducks
Subtyping and Ducks
1
Greenlets, threads, and processes
Greenlets, threads, and processes
6
Why don't you want getters and setters?
Why don't you want getters and setters?
8
The (Updated) Truth About Unicode in Python
The (Updated) Truth About Unicode in Python
1
How do I make a recursive function iterative?
How do I make a recursive function iterative?
1
Sockets and multiprocessing
Sockets and multiprocessing
Micro-optimization and Python
Micro-optimization and Python
3
Why does my 100MB file take 1GB of memory?
Why does my 100MB file take 1GB of memory?
1
How to edit a file in-place
How to edit a file in-place
ADTs for Python
ADTs for Python
5
A pattern-matching case statement for Python
A pattern-matching case statement for Python
2
How strongly typed is Python?
How strongly typed is Python?
How do comprehensions work?
How do comprehensions work?
1
Reverse dictionary lookup and more, on beyond z
Reverse dictionary lookup and more, on beyond z
2
How to handle exceptions
How to handle exceptions
2
Three ways to read files
Three ways to read files
2
Lazy Python lists
Lazy Python lists
2
Lazy cons lists
Lazy cons lists
1
Lazy tuple unpacking
Lazy tuple unpacking
3
Getting atomic writes right
Getting atomic writes right
Suites, scopes, and lifetimes
Suites, scopes, and lifetimes
1
Swift-style map and filter views
Swift-style map and filter views
1
Inline (bytecode) assembly
Inline (bytecode) assembly
Why Python (or any decent language) doesn't need blocks
Why Python (or any decent language) doesn't need blocks
18
SortedContainers
SortedContainers
1
Fixing lambda
Fixing lambda
2
Arguments and parameters, under the covers
Arguments and parameters, under the covers
pip, extension modules, and distro packages
pip, extension modules, and distro packages
Python doesn't have encapsulation?
Python doesn't have encapsulation?
3
Grouping into runs of adjacent values
Grouping into runs of adjacent values
dbm: not just for Unix
dbm: not just for Unix
How to use your self
How to use your self
1
Tkinter validation
Tkinter validation
7
What's the deal with ttk.Frame.__init__(self, parent)
What's the deal with ttk.Frame.__init__(self, parent)
1
Does Python pass by value, or by reference?
Does Python pass by value, or by reference?
9
"if not exists" definitions
"if not exists" definitions
repr + eval = bad idea
repr + eval = bad idea
1
Solving callbacks for Python GUIs
Solving callbacks for Python GUIs
Why your GUI app freezes
Why your GUI app freezes
21
Using python.org binary installations with Xcode 5
Using python.org binary installations with Xcode 5
defaultdict vs. setdefault
defaultdict vs. setdefault
1
Lazy restartable iteration
Lazy restartable iteration
2
Arguments and parameters
Arguments and parameters
3
How grouper works
How grouper works
1
Comprehensions vs. map
Comprehensions vs. map
2
Basic thread pools
Basic thread pools
Sorted collections in the stdlib
Sorted collections in the stdlib
4
Mac environment variables
Mac environment variables
Syntactic takewhile?
Syntactic takewhile?
4
Can you optimize list(genexp)
Can you optimize list(genexp)
MISRA-C and Python
MISRA-C and Python
1
How to split your program in two
How to split your program in two
How methods work
How methods work
3
readlines considered silly
readlines considered silly
6
Comprehensions for dummies
Comprehensions for dummies
Sockets are byte streams, not message streams
Sockets are byte streams, not message streams
9
Why you don't want to dynamically create variables
Why you don't want to dynamically create variables
7
Why eval/exec is bad
Why eval/exec is bad
Iterator Pipelines
Iterator Pipelines
2
Why are non-mutating algorithms simpler to write in Python?
Why are non-mutating algorithms simpler to write in Python?
2
Sticking with Apple's Python 2.7
Sticking with Apple's Python 2.7
Blog Archive
About Me
About Me
Loading
Dynamic Views theme. Powered by Blogger. Report Abuse.