[Python-ideas] A "local" pseudo-function

Tim Peters tim.peters at gmail.com
Sat Apr 28 02:07:29 EDT 2018


[Yury Selivanov <yselivanov.ml at gmail.com>]
> This is interesting. Even "as is" I prefer this to PEP 572. Below are some
> comments and a slightly different idea inspired by yours (sorry!)

That's fine :-)

> It does look like a function call, although it has a slightly different
> syntax. In regular calls we don't allow positional arguments to go after
> keyword arguments.  Hence the compiler/parser will have to know what
> 'local(..)' is *regardless* of where it appears.

Not only for that reason, but because the semantics have almost
nothing in common with function calls.  For example, in

    local(a=1, b=a+1)

the new binding of `a` needs to be used to establish the binding of
`b`.  Not to mention that a new scope may need to be established, and
torn down later.  To the compiler, it's approximately nothing like "a
function call".

"Looking like" a function call nevertheless has compelling benefits:

- People already know the syntax for specifying keyword arguments.

- The precedence of "=" in a function call is already exactly right for
  this purpose.  So nothing new to learn there either.

- The explicit parentheses make it impossible to misunderstand where
  the expression begins or ends.

- Even if someone knows nothing about "local", they _will_ correctly
  assume that, at runtime, it will evaluate to an object.  In that key
  respect it is exactly like the function call it "looks like".

I do want to leverage what people "already know".


> If you don't want to make 'local' a new keyword, we would need to make the
> compiler/parser to trace the "local()" name to check if it was imported or
> is otherwise "local". This would add some extra complexity to already
> complex code.  Another problematic case is when one has a big file and
> someone adds their own "def local()" function to it at some point, which
> would break things.

I believe it absolutely needs to become a reserved word.  While
binding expressions suffice to capture values in conditionals, they're
not all that pleasant for use in expressions (that really wants a
comma operator too), and it's a fool's errand to imagine that _any_
currently-unused sequence of gibberish symbols and/or abused keywords
can plausibly suggest "and here we're also creating a new scope, and
here I'm declaring some names to live in that scope".

If all that is actually wanted, a new reserved word seems
pragmatically necessary.  That's a high bar.

Speaking of which, "sublocal" would be a more accurate name, and less
likely to conflict with existing code names, but after people got over
the shock to their sense of purity, they'd appreciate typing the
shorter "local" instead ;-)

For that matter, I'd be fine too with shortening it to "let".  In
fact, I prefer that!  Thanks :-)


> Therefore, "local" should probably be a keyword. Perhaps added to Python
> with a corresponding "from __future__" import.

Yes.


> The other way would be to depart from the function call syntax by dropping
> the parens.  (And maybe rename "local" to "let" ;))  In this case, the
> syntax will become less like a function call but still distinct enough.  We
> will be able to unambiguously parse & compile it.  The cherry on top is
> that we can make it work even without a "__future__" import!

I'm far less concerned about pain for the compiler than pain for the
human reader.  There's almost no precedent in Python's expression
grammar that allows two names separated only by whitespace.  The
screams "statement" instead, whether "class NAME" or "async for" or
"import modulename" or "global foo" or "for i in" or ...

I'm afraid it would prove just too jarring to see that inside what's
supposed to be "an expression".  That's why I settled on the
(admittedly unexpected) "pseudo-function" syntax.

The exceptions involve expressions that are really test-&-branch
structures in disguise ("and", "or", ternary "if").  In those we can
find NAME NAME snippets, but the keyword is never at the start of
those.

So, curiously enough, I'd be fonder of

    result_expression "where" name=expression, ...

than of

    "let" name=expression, ...

if I hadn't already resigned myself to believing function-like syntax
is overwhelmingly less jarring regardless.


> When we implemented PEP 492 in Python 3.5 we did a little trick in
> tokenizer to treat "async def" in a special way. Tokenizer would switch to
> an "async" mode and yield ASYNC and AWAIT tokens instead of NAME tokens.
> This resulted in async/await syntax available without a __future__ import,
> while having full backwards compatibility.

Which was clever, and effective, but - as far as I know - limited to
_statements_, where

    KEYWORD NAME

thingies were already common as mud.


> We can do a similar trick for "local" / "let" syntax, allowing the
> following:
>
>    "let" NAME "=" expr ("," NAME = expr)* ["," expr]

See the bullet list near the top for all the questions that _raises_
that don't even need to be asked (by users) when using a function-like
syntax instead.


> * "if local(m = re.match(...), m):" becomes
>     "if let m = re.match(...), m:"
>
> * "c = local(a=3) * local(b=4)" becomes
>    "c = let a=3, b=4, a*b" or "c = (let a=3, b=4, a*b)"

I assume that was meant to be

    c = (let a=3, a) * (let b=4, b)

instead.  In _an expression_, I naturally group the

    a = 3, a

part as the unintended

    a = (3, a)

When I'm thinking of function calls, though, I naturally use the intended

    (a=3), a

grouping.  I really don't want to fight with what "everyone already
knows", but build on that to the extent possible.



> *      for i in iterable:
>            if let i2=i*i, i2 % 18 == 0:
>               append i2 to the output list
>
> etc.

In the second line there too, I did like that in

    if local(i2=i*i) % 18 == 0:

the mandatory parentheses made it wholly explicit where "the binding
part" ended.

> Note that I don't propose this new "let" or "local" to return their last
> assignment. That should be done explicitly (as in your "local(..)" idea):
>   `let a = 'spam', a`.

Why not?  A great many objects in Python are _designed_ so that their
__bool__ method does a useful thing in a plain

     if object:

test.  In these common cases, needing to type

     if let name=object, object:

instead of

     if let name=object:

is an instance of what my pseudo-FAQ called "annoyingly redundant
noise".  Deciding to return the value of the last "argument"
expression was a "practicality beats purity" thing.

>  Potentially we could reuse our function return annotation syntax, changing the
> last example to `let a = "spam" -> a` but I think it makes the whole thing to
> look unnecessarily complex.

Me too - but then I thought it was _already_ too wordy to require `let
a="spam", a` ;-)


> One obvious downside is that "=" would have a different precedence compared
> to a regular assignment statement. But it already has a different precedent
> in function calls, so maybe this isn't a big deal, considered that we'll
> have a keyword before it.

I think my response to that is too predictable by now to annoy you by
giving it ;-)


> I think that "let" was discussed a couple of times recently, but it's
> really hard to find a definitive reason of why it was rejected (or was it?)
> in the ocean of emails about assignment expressions.

I don't know either.  There were a number of halfheartedly presented
ideas, though, that floundered (to my eyes) on the rocks of trying to
ignore that syntax ideas borrowed from "everything's an expression"
functional languages don't carry over well to a language where many
constructs aren't expressions at all.  Or, conversely, that syntax
unique to the opening line of a statement-oriented language's block
doesn't carry over at all to expressions.

I'm trying to do both at once, because I'm not a wimp - and neither are you ;-)


More information about the Python-ideas mailing list