Let's talk about the choice of spelling. So far, most of the suggested 
syntaxes have modified the assignment symbol `=` using either a prefix 
or a suffix. I'm going to use '?' as the generic modifier. So the 
parameter will look like:

    # Modifier as a prefix
    param?=expression
    param:Type?=expression

    # Modifier as a suffix
    param=?expression
    param:Type=?expression

One problem with these is that (depending on the symbol used), they can 
be visually confused with existing or possible future features, such as:

* using a colon may be confusable with a type hint or walrus operator;

* using a greater-than may be confusable with the proposed Callable 
  sugar -> or lambda sugar =>
  
* as well as just plain old greater-than;

* using a question mark may be confusable with hypothetical 
  None-aware ??= assignment.

By confusable, I don't mean that a sophisticated Python programmer who 
reads the code carefully with full attention to detail can't work out 
what it means.

I mean that new users may be confused between (say) the walrus `:=` and 
"reverse walrus" `=:`. Or the harrassed and stressed coder working at 
3am while their customer on the other side of the world keep messaging 
them. We don't always get to work carefully with close attention to 
detail, so we should prefer syntax that is less likely to be confusable.

So far, I dislike all of those syntaxes (regardless of which symbol is 
used as the modifier). They are all predicated on the idea that this is 
a new sort of assignment, which I think is the wrong way to think about 
it. I think that the better way to think about it is one of the 
following:

1. It's not the assignment that is different, it is the expression
   being bound.

2. It is not the assignment that is different, it is the parameter.

Suggestion #1 suggests that we might want a new kind of expression, 
which for lack of a better term I'm going to call a thunk (the term is 
stolen from Algol). Thunks represent a unit of delayed evaluation, and 
if they are worth doing, they are worth doing anywhere, not just in 
parameter defaults. So this is a much bigger idea, and a lot more 
pie-in-the-sky as it relies on thunks being plausible in Python's 
evaluation model, so I'm not going to talk about #1 here.

Suggestion #2 is, I will argue, the most natural way to think about 
this. It is the parameter that differs: some parameters use early 
binding, and some use late binding. Binding is binding, regardless of 
when it is performed. When we do late binding manually, we don't do 
this:

    if param is None:
        param ?= expression  # Look, it's LATE BINDING assignment!!!

So we shouldn't modify the assignment operator. It's still a binding. 
What we want to do is tell the compiler that *this parameter* is 
special, and so the assignment needs to be delayed to function call time 
rather than function build time. We can do that by tagging the 
parameter.

What's another term for tagging something? Decorating it. That suggests 
a natural syntax:

    # arg1 uses early binding, arg2 uses late binding
    def function(arg1=expression, @arg2=expression):

And with type annotations:

    def function(arg1:Type=expression, @arg2:Type=expression) -> Type:

I know it's not an actual decorator, but it suggests the idea that we're 
decorating the parameter to use late binding instead of early.

Advantages:

- The modifer is right up front, where it is obvious.

- Doesn't look like grit on the monitor.

- It can't be confused with anything inside the type hint or 
  the expression.

- No matter how ludicrously confusing the annotation or expression
  gets, the @ modifier still stands out and is obvious.

- Forward-compatible: even if we invent a prefix-unary @ operator
  in the future, this will still work:

    def function(@param:@Type=@expression)

- Likewise for postfix unary operators:

    def function(@param:Type@=expression@)

Here's a trivial advantage: with the "modify the equals sign" syntax, if 
you decide to copy the assignment outside of the function signature, you 
are left with a syntax error:

    def function(param?=expression)
    # double-click on "param", drag to expression, copy and paste
    
    param?=expression  # SyntaxError

Its not a big deal, but I can see it being a minor annoyance, especially 
confusing for newbies. But with a leading @ symbol, you can double-click 
on the param name, drag to the expression, copy and paste, and in most 
GUI editors, the @ symbol will not be selected or copied.

    def function(@param=expression)
    # double-click on "param", drag to expression, copy and paste
    
    param=expression  # Legal code.

(I don't know of any GUI editors that consider @ to be part of a word 
when double-clicking, although I suppose there might be some.)

Disadvantages:

- Maybe "at symbol" is clunkier to talk about than "arrow operator" or 
  "reverse walrus"?

- Search engines aren't really great at searching for the at symbol:

    https://www.google.com.au/search?q=python+what+does+%40+mean

    https://duckduckgo.com/?q=python+what+does+%40+mean

DDG gives the top hit a Quora post about the at symbol, but everything 
else is a miss; Google is even worse. But then any other symbol is going 
to be subject to the same problem.

Looking back at the "modify the equals" syntax, it puts the important 
information right there in the middle of something which could be an 
extremely busy chunk of text:

    param:Optional[Callable[TypeA, TypeB, Bool]]=>lambda a, b: a>lo and b>hi

Even if it is syntactically unambiguous, and not confused with anything 
else, it is still not obvious. It doesn't stand out when skimming the 
code. And we all sometimes just skim code.

    @param:Optional[Callable[TypeA, TypeB, Bool]]=lambda a, b: a>lo and b>hi


Let's have a look at some real cases from the stdlib:

    # bisect.py
    def bisect_right(a, x, lo=0, @hi=len(a), *, key=None):

    # calendar.py
    class LocaleTextCalendar(TextCalendar):
        def __init__(self, firstweekday=0, @locale=_locale.getdefaultlocale()):

    # copy.py
    def deepcopy(x, @memo={}, _nil=[]):

    # pickle.py
    class _Pickler:
        def __init__(self, file, @protocol=DEFAULT_PROTOCOL, *, 
                     fix_imports=True, buffer_callback=None):

(Note: some of these cases may be backwards-incompatible changes, if the 
parameter is documented as accepting None.)


-- 
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/RQQQWH2K3BXCXCBUX5ZZBBRFYLXDN7TK/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to