
On Nov 29, 2017 10:09 PM, "Steven D'Aprano" <steve@pearwood.info> wrote:
On Thu, Nov 30, 2017 at 11:21:48AM +1300, Greg Ewing wrote:
It seems that many people think about unpacking rather differently from the way I do. I think the difference is procedural vs. declarative.
To my way of thinking, something like
a, b, c = x
is a pattern-matching operation. It's declaring that x is a sequence of three things, and giving names to those things. It's not saying to *do* anything to x.
I hadn't thought of that interpretation before, but now that Greg mentions it, its so obvious-in-hindsight that I completely agree with it. I think that we should promote this as the "one obvious" interpretation.
Obviously as a practical matter, there are some x (namely iterators) where you cannot extract items without modifying x, but in all other cases I think that the pattern-matching interpretation is superiour.
This conversation about suitcases, matching, and language assumptions is interesting. I've realized two concrete things about how I understand unpacking, and perhaps, further explain the dissonance we have here: * Unpacking is destructuring not pattern matching. * Tuple syntax is commas, paren, one, or both. For the former, destructuring, this reply conveys my thoughts verbatim: https://groups.google.com/forum/#!topic/clojure/SUoThs5FGvE "There are two different concerns in what people refer to as "pattern matching": binding and flow-control. Destructuring only addresses binding. Pattern matching emphasizes flow control, and some binding features typically come along for free with whatever syntax it uses. (But you could in principle have flow control without binding.)" The only part of unpacking that is 'pattern matching' is the fact that it blows up spectacularly when the LHS doesn't perfectly match the length of RHS, reversing flow via exception:
0,b = 0,1 File "<stdin>", line 1 SyntaxError: can't assign to literal
If Python really supported pattern matching (which I would 100% love! yes please), and unpacking was pattern matching, the above would succeed because zero matches zero. Pattern matching is used extensively in Erlang/Elixir for selecting between various kinds of clauses (function, case, etc), but you also see *significant* use of the `[a, b | _] = RHS` construct to ignore "the remainder" because 99% of the time what you really want is to [sometimes!] match a few things, bind a few others, and ignore what you don't understand or need. This is why destructuring Elixir maps or JS objects never expect (or even support AFAIK) exact-matching the entire object... it would render this incredible feature next to useless! *Destructuring is opportunistic if matching succeeds*. For the latter, tuples-are-commas-unless-they-are-parens :-), I suspect I'm very much in the minority here. While Python is one of my favorite languages, it's only 1 of 10, and I didn't learn it until I was already 4 languages deep. It's easy to forget how odd tuples are because they are so baked in, but I've had the "well, ehm, comma is the tuple constructor... usually" or "well, ehm, you are actually returning 1 tuple... not 2 things" conversation with *dozens* of seasoned developers. Even people professionally writing Python for months or more. Other languages use more explicit, visually-bounded object constructors. This makes a meaningful difference in how a human's intuition interprets the meaning of a new syntax. *Objects start and end but commas have no inherent boundaries*. These two things combined lead to unpacking problems because I look at all assignments through the lens of destructuring (first observation) and unpacking almost never uses parentheses (second observation). To illustrate this better, the following is how my mind initially parses different syntax contrasted with what's actually happening (and thus the correction I've internalized over a decade):
a, b = 5, 6 CONCEPT ... LHS[0] `a` bound to RHS[0] `5`, LHS[1] `b` bound to RHS[1] `6`. REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple.
a, b = 5, 6, 7 CONCEPT ... LHS[0] `a` bound to RHS[0] `5`, LHS[1] `b` bound to RHS[1] `6`, RHS[2] `6` is unbound expression. REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple, bad match, RAISE ValueError!
(a, b) = 5, 6 [a, b] = 5, 6 CONCEPT ... LHS[0] `(a, b)` bound to RHS[0] `5`, bad match, RAISE TypeError! REALITY ... LHS[:] single tuple destructured to RHS[:] single tuple.
a, b = it [a], b = it CONCEPT ... LHS[0] `a` bound with RHS[0] `it`, LHS[1] is bad match, RAISE UnboundLocalError/NameError! REALITY ... `a` bound to `it[0]` and `b` bound to `it[1]` (`it[N]` for illustration only!)
The tuple thing in particular takes non-zero time to internalize. I consider it one of Python's warts, attributed to times explained and comparisons with similar languages. Commas at the top-level, with no other construction-related syntax, look like expression groups or multiple returns. You have to already know Python's implicit tuple quirks to rationalize what it's really doing. This helps explain why I suggested the `LHS0, LHS1 = *RHS` syntax, because it would read "expand RHS[0] into RHS[:]". Thanks, -- C Anthony