
On Sat, Apr 18, 2020 at 5:03 PM Steven D'Aprano <steve@pearwood.info> wrote:
On Sat, Apr 18, 2020 at 02:13:51PM +0200, Alex Hall wrote:
My issue with this, and maybe it's what Andrew is also trying to say, is that it breaks our usual assumptions about composing expressions. `{u, v}` is an expression, it represents a set, and it always represents that wherever you put it. Under your proposal, these two programs are both valid syntax with different meanings:
f(**{u, v})
x = {u, v} f(**x)
True, and that does count as a (minor?) point against it. But not one that I think should rule it out.
Is there anything else similar in the language? Obviously there are cases where the same text has different meanings in different contexts, but I don't think you can ever refactor an expression (or text that looks like an expression) into a variable and change its meaning while keeping the program runnable.
Of course! There are many ways that this can occur.
OK, I have to admit I had a bit of a failure of imagination. But there is something about all these examples (plausibility?) that feels distinctly different from your proposal. I can't quite put my finger on it. It might just be that your proposal is new and I need to get used to it the way I'm used to how the rest of Python works. But I'm not convinced of that.
f(a, b, c)
x = a, b, c f(x)
are very different things.
This imagined refactoring doesn't feel as plausible. A complete beginner might think that they can do that, but a programmer who knows what tuples are can reason that it doesn't make sense. On the other hand, an intermediate programmer can be forgiven for thinking that they could refactor out the {u, v} in the proposed syntax. Hypothetically, sets could support the mapping protocol. Understanding that it's impossible requires understanding the guts of the language better and that variable names aren't stored with objects at runtime. Put differently, the proposal could mislead readers into thinking that sets do actually know the names of the values in them, without them trying to refactor out the 'set'.
Here's another one:
import name
x = name import x
This is stretching the idea of "text that looks like an expression". 'name' looks like an expression in complete isolation, but even a beginner who has just learned imports knows (at least at an intuitive, informal level) that 'name' is not an expression that is evaluated to determine what to import. This is generally obvious from the fact that 'name' is usually not defined beforehand. So even someone who just knows the basics of variables and hasn't seen imports before might guess that the refactoring would fail (with a NameError). Another litmus test that it isn't like an expression is that `import (name)` isn't valid.
Here's a third:
del fe, fi, fo, fum
x = fe, fi, fo, fum del x
This could be simplified to del y vs x = y del x This actually is something that a beginner might think would work, and several have probably tried. The workings of variables, references, names, scope, and garbage collection are confusing at first. We shouldn't introduce new features that are similarly confusing.
Here's an example so obvious (and trivial) that I'm almost embarrassed to include it:
seq[index]
x = [index] seqx
Again, '[index]' only looks like a list in complete isolation. If you try to interpret it as a list while it's stuck to 'seq', the code doesn't even begin to make sense. You can't just stick two expressions together, there has to be an operator or something in between. And the refactoring isn't plausible - it's obvious that seqx is a new variable and not the concatenation of seq and x, otherwise it could equally be s|eqx or se|qx or s|e|q|x.
If code is made of composable building blocks, those blocks aren't *characters*. What makes a composable block is dependent on context.
To make up for how trivial the previous example was, here's a complicated one:
for item in items: if item: continue do_stuff()
versus:
def block(item): if item: continue do_stuff()
for item in items: block()
I have often wished I could refactor continue and break into functions, but you can't :-(
Me too :-(
Although in this case at least you get a syntax error when you try.
Therefore it doesn't satisfy my criteria.
Here's an example with sequence unpacking:
a = [1, 2, *seq]
x = 2, *seq a = [1, x]
The problem here isn't related to unpacking. This doesn't work for the same reasons: a = [1, 2, 3] x = 2, 3 a = [1, x] or just: a = [1, 2] x = 1, 2 a = [x] Which shows that it's basically the same as the function call example at the beginning. If you understand how lists and tuples work, you can understand why the refactoring doesn't work. Put differently, it's not just about refactoring out variables. The above refactoring is equivalent to changing: a = [1, 2, 3] to a = [1, (2, 3)] And it's pretty obvious to a beginner that the two expressions are different. It's much less obvious that these are different: f(**{u, v}) f(**({u, v})) Tuples in Python are weird. They're supposed to be defined by the comma, but they're obviously not really, because commas have all sorts of other meanings, and empty tuples don't need commas. They need parentheses sometimes in cases that are hard to summarise. People think that parentheses are what define the tuple and don't realise they need a trailing comma for singletons. It's a mess, and again, we should avoid creating things which are similarly confusing.
Another example:
class MyClass(metaclass=MyMeta)
metaclass = MyMeta class MyClass(metaclass)
No, this hasn't refactored `metaclass=MyMeta` out into a variable, you've just moved it. The correct analogy would be: M = metaclass=MyMeta class MyClass(M) which makes the same point, but still violates the rules because `metaclass=MyMeta` isn't text that looks like an expression.
That's just a special case of keyword notation itself:
func(x=expr)
x = expr func(x)
Indeed it is just a special case, and it has the same problem. x=expr is not text that looks like an expression. As you say below, you cannot for example put it into parentheses.
Those are not the same, unless the first positional argument happens to be named `x`.
And one final example:
class C: def method(self): pass
versus:
def method(self): pass
class C: method
`def method...` is not an expression.
The example above looks a bit dumb, but maybe users will try:
``` if flag: kwargs = {u, v} else: kwargs = {w, x} f(**kwargs) ```
I think this point will apply to all(?) such syntactic proposals. I don't think this scenario is too different from this:
if flag: f(u=expr1, v=expr2) else: f(w=expr3, x=expr4)
If you want to refactor that, you can't do this:
f((u=expr1, v=expr2) if flag else (w=expr3, x=expr4))
No you can't, so thankfully it raises a SyntaxError, unlike your proposal.
Which is valid syntax but is wrong. Then they might try changing that to:
f(**({u, v} if flag else {w, x}))
which is suddenly invalid syntax.
Not invalid syntax, but it's still wrong. You'll get a TypeError when trying to `**` unpack a set instead of a dict.
Yes I got confused and made a mistake here. The fact that it doesn't raise a SyntaxError is still bad, for the same reasons that I argued about in the beginning (whereas previously I thought it was bad for new and different reasons). The point is that the syntax looks and feels like it can be manipulated like an expression.