Backquotes for deferred expression

One tricky problem will be: if you modify a variable that’s not local to the micro function (i.e. DeferExpr), will it affect the original value?

Analogous to:

x = 1
l = [x for x in range(10)]
# now what's x?

d = `x = x + 1`
print(d, x) # will `x` be affected?

AFAIK this has just been addressed in 3.13 so outside x will remain unchanged. IMO it’s best to make any new syntax behave the same as the list iteration example.

Then it won’t work for function default arguments the way you show. Adding context to your example:

x, y = 1, 2
def fn(x: int, y: int, z: int = `x + y`):
    print(z) # expr will be evaluated here

fn(3, 4)

This will print 3, not 7. You can try this with a lambda function today, since you’ve defined the semantics identically:

x, y = 1, 2
def fn(x: int, y: int, z = lambda: x + y):
    print(z())

fn(3, 4)

I gave an example. Can you explore its semantics please? What triggers evaluation? Here, I’ll flesh out the example with some numbers.

def inner_scope(): # this might be a separate module or something
    a, b = 10, 2
    def func(stuff):
        print(stuff)
        print(stuff)
    return func

func = inner_scope() # similarly, this could be a module import
a, b = 20, 3
def do_something()
    a, b = 30, 4
    x = `a + b`
    print(x)
    func(x)
    func(x)
do_something()

Choose some semantics and tell me what this would do.

True, I acknowledged that this shouldn’t be used to sell the proposal in previous conversation:


This is a perfect example of why I described it as a zero-argument lambda (which invokes itself automatically upon observation). I can easily rewrite your code and tell you how it would behave:

def inner_scope(): # this might be a separate module or something
    a, b = 10, 2 # out of scope, don't matter
    def func(stuff):
        print(stuff) # print(x()) => 30 + 4
        print(stuff) # print(x()) => 30 + 4
    return func

func = inner_scope() # similarly, this could be a module import
a, b = 20, 3
def do_something()
    a, b = 30, 4
    x = lambda: a + b
    print(x())
    func(x) # argument passing is NOT observation
    func(x) # same
do_something()

I’m glad that you brought this up!

I can give you a more complicated example:

def factory(x):
    def add(y):
        print(x + y)
    return add

a, b = -1, -2

def alg():
    # Note a and b are not yet available in locals
    # Their names are registered in AST but yet assigned a value
    c = `a + b`
    # Eq: c = lambda: a + b

    add = factory(c)

    # This will cause NameError, NOT print(-3)
    print(c)
    # Eq: print(c())

    a, b = 1, 2
    add(1) # 3
    # Eq: print(c() + 2)

    a, b = 3, 4
    add(2) # 9
    # Eq: print(c() + 2)

By following the “zero argument lambda” analogy, it’s behaviour is very easy to explain. It will be as intuitive as actually using a lambda function.

Why? What’s special about print?

Because print will invoke str(x) repr(x). This constitutes an observation.

So what’s special about str()? Why can’t you get the string representation of the deferred object itself?

Which functions are “observations” and which ones are not? What is the rule here? For example, is x.attr going to look at the deferred object or the result of the expression? What about getattr(x, "attr")?

You describe this as intuitive, but you can’t handwave these sorts of distinctions away. What exactly is the rule for whether it’s evaluated or not?

This is exactly why implementation details (2) is there. I do not have any preference among the two options. Option 1 is already used by third party wheels but is prone to attribute conflicts, option 2 is simpler and more elegant, but lacks some information that you could access on lambda functions (I.e closures)

You can’t handwave things away by saying “the semantics can be whatever you want them to be”. Choose something and stick to it. You have to have a concrete proposal or we’re just trying to nail jelly to a wall [1].

What are you proposing?


  1. again ↩︎

1 Like

They’re not free to be reused, though – they’ve been banned on the
grounds that they look like grit on Tim’s monitor. So to get this
proposal accepted, you will have to resolve that somehow.

3 Likes

I think you should prototype before proposing. If you have at least 51% baked proposal, then it is possible to discuss. Now, the scope is so wide that it is difficult to start discussing this.

Or if you want others to help you brainstorm, then best to use “help”, not “ideas”.

I, personally, don’t mind as long as the OP is aware that his idea is in a baby state.

But this has been an issue in the past and consensus was that posting proposals that have no definite proof of concept and loose ends on major decisions are not very welcome in “ideas”.

1 Like

lol

That’s the last symbol on my keyboard which is available for a new syntax…

Which is why keywords are better choices for most proposals.

1 Like

I will try to get something to work asap. No promise though since I will spend a lot of time struggling to understand the source code.

You don’t have to have something runnable (although that would certainly help). What you need is clear and well-defined semantics. It may help to spend some time writing code as if your new proposal worked, and try to find all the edge cases.

1 Like

I feel like we need an FAQ so that each time deferred evaluation comes up, we can just point at it and say “please read this”.

The great failing of many of these proposals is, IMO, refusal to define and construct an explicit new type. People want it to be magically transparent, which is what then ties them in knots.
If you accept that it is a special object like super(), you can actually define some semantics. They might not be much better than a lambda, but at least they can plausibly be defined and even prototyped in Python itself. And I think super() shows that you can make a super[1] cool object if you’re clever about it.


  1. sorry ↩︎

1 Like

That would be a rejected PEP, you want to write one?

[1]


  1. no you’re not ↩︎

1 Like

Python was originally developing in the opposition to Perl. If you use too many magical characters in your syntax, you make many old-timers cringe.

I am making slow progress on modifying the CPython source code to create a demo. I am trying to find inspirations from this commit and find a path to achieve that I envisioned.

I now agree more with @dg-pb that the syntax itself does not matter much. I have the following syntaxes in my mind, I will try to implement whichever that turns out to be within my reach.

x = `1 + 2` # OP
x => 1 + 2  # Extended use of PEP671
x = d"1 + 2" # f-string like style