On 2018-04-27 11:37 PM, Tim Peters wrote:
A brain dump, inspired by various use cases that came up during the binding expression discussions.
Idea: introduce a "local" pseudo-function to capture the idea of initialized names with limited scope.
As an expression, it's
"local" "(" arguments ")"
- Because it "looks like" a function call, nobody will expect the targets of named arguments to be fancier than plain names.
- `a=12` in the "argument" list will (& helpfully so) mean pretty much the same as "a=12" in a "def" statement.
- In a "local call" on its own, the scope of a named argument begins at the start of the next (if any) argument, and ends at the closing ")". For the duration, any variable of the same name in an enclosing scope is shadowed.
- The parentheses allow for extending over multiple lines without needing to teach editors (etc) any new tricks (they already know how to format function calls with arglists spilling over multiple lines).
- The _value_ of a local "call" is the value of its last "argument". In part, this is a way to sneak in C's comma operator without adding cryptic new line noise syntax.
Time for an example. First a useless one:
a = 1 b = 2 c = local(a=3) * local(b=4)
Then `c` is 12, but `a` is still 1 and `b` is still 2. Same thing in the end:
c = local(a=3, b=4, a*b)
And just to be obscure, also the same:
c = local(a=3, b=local(a=2, a*a), a*b)
There the inner `a=2` temporarily shadows the outer `a=3` just long enough to compute `a*a` (4).
This is one that little else really handled nicely:
r1, r2 = local(D = b**2 - 4*a*c, sqrtD = math.sqrt(D), twoa = 2*a, ((-b + sqrtD)/twoa, (-b - sqrtD)/twoa))
Everyone's favorite:
if local(m = re.match(regexp, line)): print(m.group(0))
Here's where it's truly essential that the compiler know everything about "local", because in _that_ context it's required that the new scope extend through the end of the entire block construct (exactly what that means TBD - certainly through the end of the `if` block, but possibly also through the end of its associated (if any) `elif` and `else` blocks - and similarly for while/else constructs).
Of course that example could also be written as:
if local(m = re.match(regexp, line), m): print(m.group(0))
or more specifically:
if local(m = re.match(regexp, line), m is not None): print(m.group(0))
or even:
if local(m = re.match(regexp, line)) is not None: print(m.group(0))
A listcomp example, building the squares of integers from an iterable but only when the square is a multiple of 18:
squares18 = [i2 for i in iterable if local(i2=i*i) % 18 == 0]
That's a bit mind-bending, but becomes clear if you picture the kinda-equivalent nest:
for i in iterable: if local(i2=i*i) % 18 == 0: append i2 to the output list
That should also make clear that if `iterable` or `i` had been named `i2` instead, no problem. The `i2` created by `local()` is in a wholly enclosed scope.
Drawbacks: since this is just a brain dump, absolutely none ;-)
Q: Some of those would be clearer if it were the more Haskell-like
local(...) "in" expression
A: Yup, but for some of the others needing to add "in m" would be annoyingly redundant noise. Making an "in" clause optional doesn't really fly either, because then
local(a='z') in 'xyz'
would be ambiguous. Is it meant to return `'xyz'`, or evaluate `'z' in 'xyz'`? And any connector other than "in" would make the loose resemblance to Haskell purely imaginary ;-)
Q: Didn't you drone on about how assignment expressions with complex targets seemed essentially useless without also introducing a "comma operator" - and now you're sneaking the latter in but _still_ avoiding complex targets?!
A. Yes, and yes :-) The syntactic complexity of the fully general assignment statement is just too crushing to _sanely_ shoehorn into any "expression-like" context.
Q: What's the value of this? local(a=7, local(a=a+1, a*2))
A: 16. Obviously.
Q: Wow - that _is_ obvious! OK, what about this, where there is no `a` in any enclosing scope: local(a)
A: I think it should raise NameError, just like a function call would. There is no _intent_ here to allow merely declaring a local variable without supplying an initial value.
Q: What about local(2, 4, 5)?
A: It should return 5, and introduce no names. I don't see a point to trying to outlaw stupidity ;-) Then again, it would be consistent with the _intent_ to require that all but the last "argument" be of the `name=expression` form.
Q: Isn't changing the meaning of scope depending on context waaaay magical?
A: Yup! But in a language with such a strong distinction between statements and expressions, without a bit of deep magic there's no single syntax I can dream up that could work well for both that didn't require _some_ deep magic. The gimmick here is something I expect will be surprising the first time it's seen, less so the second, and then you're never confused about it again.
Q: Are you trying to kill PEP 572?
A: Nope! But since this largely subsumes the functionality of binding expressions, I did want to put this out there before 572's fate is history. Binding expressions are certainly easier to implement, and I like them just fine :-)
Note: the thing I'm most interested in isn't debates, but in whether this would be of real use in real code. _______________________________________________ Python-ideas mailing list Python-ideas@python.org https://mail.python.org/mailman/listinfo/python-ideas Code of Conduct: http://python.org/psf/codeofconduct/
Why not non-lexical variables? Basically, make this work (and print 3): def test(): i = 3 def test_inner(): print(i) hide i i = 4 test_inner() # 3 print(i) # 4 `hide`, unlike `del`, only applies to the current scope, and only forward. it does what it says on the tin: makes the variable disappear/be hidden. ofc, python doesn't support shadowing of variables, so this is kinda useless, but eh I thought it was a cool idea anyway :/ (See also this thread on the Lua mailing list: https://marc.info/?l=lua-l&m=152149915527486&w=2 )