On Tue, May 25, 2021 at 12:30 AM Steven D'Aprano <steve@pearwood.info> wrote:

Your proposal appears to be:

    @decorator(expression) targetname

    # transformed into:

    targetname = decorator("targetname", expression)

But in class and function decorator contexts, the equivalent syntax is
equivalent to:

    @decorator("spam this")
    def func(): pass

    # transformed to:

    def func(): pass
    func = decorator("spam this")(func)

except that in CPython it is not actually implemented as two separate
steps like that.

But the critical difference is that the argument "spam this" should be
passed to the decorator *factory*, which then returns the actual
decorator that gets applied to the variable. (Or function/ class in the
case of regulator decorator syntax.)

Yes, you are correct that the current only use of a decorator, if it takes
arguments, first executes the decorator factory and then passes in the
function object. This proposal is more restrictive, and always calls the
decorator once, whether parentheses follow the decorator name or not.
I suppose the same could be done for variable decorating. It would look
something like:

@namedtuple("x y z") Point

would translate to

Point = namedtuple("x y z")("Point")

only namedtuple doesn't work that way, so a new decorator factory
would have to be created

@namedtuple_declare("x y z") Point

where namedtuple_declare worked something like this?

def namedtuple_declare(field_names, *, rename=False, defaults=None, module=None):
    def delayed_namedtuple(typename):
        return namedtuple(typename, field_names, rename=rename, defaults=defaults, module=module)
    return delayed_namedtuple

Which maybe would turn out to be useful as you could pre-bake the decorator
function. Others may feel differently but I don't find that as compelling as the
original restricted proposal.

# now possible, but what does this get you over different instances of the same
# class?
point_tuple = namedtuple_declare("x y z")
@point_tuple Origin
@point_tuple Inflection
@point_tuple Point

Of course even following the function decorators, the most basic uses would
still act the same.

@str GREEN
@symbols x

However, a lot of the "for free" uses I outlined would no
longer be possible, and new factory functions would have to be added to the
standard lib to facilitate creating a variable decorator for these use cases.

I realize the original proposal acts differently than current decorators, it's not
currently valid to have the @ on the same line as the def, so until now it was
inherently a multi-line statement but I was proposing a one-line only statement.
The use of @ is not necessary to the overall proposal. I chose it because
current users of python would understand that somewhere there is a function
of the same name that is "getting in the way" of the binding. Also it visually
draws attention to itself when scanning down the left hand side unlike some
other options such as "as" which has historically been the only use of bind to
the right.

To my mind, the only interesting part of this proposal is access to
the binding target name. If we remove that from the proposal, variable
decorators are nothing more than function calls, and we already can do

    variable = decorator("spam this")

Yes, this was the only reason I had to bring this proposal forward. Although
it could be argued that function decorators are still only function calls, they
have proven to be popular and IMO create cleaner reading code.
Here's a counter-proposal: we have a special symbol which is transformed
at compile-time to the left hand assignment target as a string. Let's
say we make that special expression `@@` or the googly-eyes symbol.

Very interesting idea. I will say that turning this proposal back into something
to be used in assignment does expand it reach, by how much would have to
be decided, and can make for more confusion. You bring up
spam = eggs = cheese = func(arg, @@)
but what about
spam = (eggs := func(@@, flags))
I think I know what the @@ is supposed to turn into. At least I do if @@ is
valid in assignment expressions. If it is not, did I remember that it always
binds to the assignment statement name?

I will say that this offers more flexibility in that the binding name can be
reused anywhere on the lhs, although I don't think it's common to have
these sorts of factories where the first argument isn't the name. I think
it does place a larger cognitive burden on the developer (very small,
but still larger IMO). They will still have to remember what the symbol
is for name replacement and be willing to type that in instead.
Will python users reach for this in the simple case? RED = "RED" is
maybe faster to get out of my fingers than pausing and writing
RED = @@. Will some scripts start to look like this?

PATH = os.getenv("PATH")
PYTHONHOME = os.getenv("PYTHONHONE")  # oops!

This is maybe overly critical, and I realize there is nothing stopping
decorators from ending up just like this, but I think that if a developer
starts out writing an assignment statement they might just finish it
without reaching for @@. Starting a new line with a decorator is a
distinct way of binding names.
Some things would be more intuitive though, especially around
dicts and general uses of square brackets.

header = message.get(@@)
@message.get header

verbose = @@ in options
@operator.contains(options) verbose

    RED = @@
    # transforms to `RED = 'RED'`
If there's no target, it resolves to None:
Chained assignments transform to a tuple of target names:
Sequence unpacking assignment gets transformed as a single
comma-seperated string:

This feels like a lot of rules to keep straight, and a lot of verbose
checking required for factory functions that want to support all three
(four?! I would also be in favor of SyntaxError) possibilities. 

Target resolution is performed at compile-time, not runtime. There's no
global variable called "@@". That means that this won't work:

    code = compile("type(@@, bases, namespaces)", '', 'single')
    # above transforms to `type(None, bases, namespace)`
    myclass = eval(code)

But I think that restriction is fine.


A very interesting counter. Lots to consider here, appreciate the detailed reply!