On Wed, Dec 1, 2021 at 9:09 PM Steven D'Aprano firstname.lastname@example.org wrote:
On Wed, Dec 01, 2021 at 07:07:20PM +1100, Chris Angelico wrote:
def process(func:List->int=>xs=>expression)->int: ...
I'm not sure what that's supposed to mean.
You did a pretty good job of working it out :-)
Okay, cool, thanks.
Exactly my point. Beyond your arrow syntax for default parameters, and the existing return annotation use, there are two other hypothetical proposals for arrow syntax on the table:
using `->` as an alias for typing.Callable;
using `=>` as a more compact lambda;
That confuses me. Why would they use different arrows?
If we have a compact lambda syntax, why can't actual functions be used as annotations to represent functions? I believe there's plans to have [str] mean List[str], which makes a lot of sense. Why not have "lambda str: int" or "str=>int" be an annotation too?
But okay. Supposing that annotations have to use one arrow and lambda functions use another, then yes, this is a good reason to use "=:" for late-bound defaults. It's not that big a change.
func:ann=>dflt # late-bound default, completely unnecessary here
Come on Chris, how can you say that it is "completely unnecessary"? Unless that's an admission that late-bound defaults are all unnecessary... *wink*
There is a difference between these two:
def func(arg=lambda a: expression): ...
def func(arg=None): if arg is None: arg = lambda a: expression ...
therefore there will be a difference between:
def func(arg=lambda a: expression): def func(arg=>lambda a: expression):
If nothing else, in the first case (early binding) you get the same function object every time. In the second, you get a freshly made function object each time. Since function objects are mutable (they have a writable `__dict__` that's a visible difference even if the bodies are identical. And they may not be.
Yyyyyes. Okay. There is a technical way in which you might want this alternate behaviour. Is that REALLY something you're planning on doing - having a function which takes another function as an argument, and which behaves differently based on whether it's given the same function every time or multiple different functions with the same behaviour?
But even if it is unnecessary, it will still be permitted, just as we will be able to write:
# Now this *actually is* totally unnecessary use of late-binding def func(arg=>None):
Yes. That one is absolutely unnecessary, since there is no way you'll ever get back a different result. Except that you could then mutate func's dunders and change the behaviour. So in a purely technical sense, nothing can be called "unnecessary".
Now, in real terms: late-binding a lambda function with no default arguments seems like a pretty pointless thing to do. I stand by my original statement :) And I also stand by my original statement that proper use of the space bar can not only improve readability, it can also help you find a date with space girls and space guys... or maybe that last part only works on Mars.
xs=>expression # presumably a lambda function def process(args)->int # return value annotation
Why should List->int and xs=>expression use different arrows?
Because they do different things. To avoid confusion.
Chris, you wrote the PEP for the walrus operator. Why should the assignment operator use a different symbol from the assignment statement? Same reason that typing.Callable and lambda will likely use different arrows.
Anyway, if you disagree, take it up with Guido, it was his suggestion to use different arrows :-P
Fair enough, but typing also started out with List[str] and is now looking at using list[str] and, I think, [str], because it's more consistent :)
Wouldn't it be much more reasonable for them to use the same one, whichever that be? And if that does turn out to be "=>", then yes, I would be looking at changing PEP 671 to recommend := or =: or something, for clarity (but still an equals sign with one other symbol next to it).
Oh great, now you're going to conflict with walrus...
def process(obj:Union[T:=something, List[T]]:=func(x:=expression)+x)->T:
We ought to at least try to avoid clear and obvious conflicts between new and existing syntax.
Using `:=` is even worse than `=>`, and `=:` is just *begging* to confuse newcomers "why do we have THREE different assignment symbols?"
If you look at every way the equals sign is used in Python grammar, there are a lot of subtle differences. The difference between "f(x=1)" and "x=1" doesn't bother people, and those are exactly the same symbol. There are only so many symbols that we can type on everyone's keyboards.
But you're right, and that's why I would very much like to reuse an existing symbol rather than create new ones.
It's always possible to come up with pathological code. But this is only really as bad as you describe if it has zero spaces in it. Otherwise, it's pretty easy to clarify which parts go where:
def process(func: List->int => xs=>expression) -> int: ...
"My linter complains about spaces around operators! Take them out!"
Yes, take your linter out the back and execute it :)
Or maybe we should put more in? Spaces are optional.
def process(func : List -> int => xs => expression) -> int: ...
I'm not saying that an experienced Pythonista who is a careful reader can't work out what the meaning is. It's not *ambiguous* syntax to a careful reader. But it's *confusing* syntax to somebody who may not be as careful and experienced as you, or is coding late at night, or in a hurry, or coding while tired and emotional or distracted.
We should prefer to avoid confusable syntax when we can. The interpreter can always disambiguate assignment as an expression from assignment as a statement, but we still chose to make it easy on the human reader by using distinct symbols.
would be totally unambiguous to the intepreter, but I trust you would reject that because it reads like "greater than" to the human reader.
Actually that, to me, is no different from the other options - it's still an arrow (but now more like a funnel than an arrow, which is a bit weird). It's no more a greater-than than "=>" is.