On Tue, Jun 21, 2022 at 12:13:08AM +1000, Chris Angelico wrote:
Nice analogy. It doesn't hold up.
Consider this function:
def f(stuff, max=>len(stuff)): stuff.append(1) print(max)
f([1,2,3])
How would you use lazy evaluation to *guarantee* the behaviour here?
By "the behaviour" I presume you want `max` evaluated before the body of the function is entered, rather than at its point of use. Same way your implementation does: ensure that the interpreter fully evaluates `max` before entering the body of the function.
The only way I can imagine doing it is basically the same as I'm doing: that late-bound argument defaults *have special syntax and meaning to the compiler*. If they were implemented with some sort of lazy evaluation object, they would need (a) access to the execution context, so you can't just use a function;
Obviously you can't just compile the default expression as a function *and do nothing else* and have late bound defaults magically appear from nowhere. Comprehensions are implemented as functions. Inside comprehensions, the walrus operator binds to the caller's scope, not the comprehension scope. >>> def frob(items): ... thunk = ((w:=len(items)) for x in (None,)) ... next(thunk) ... return ('w' in locals(), w) ... >>> frob([1, 2, 3, 4, 5]) (True, 5) That seems to be exactly the behaviour needed for lazy evaluation thunks, except of course we don't need all the other goodies that generators provide (e.g. send and throw methods). One obvious difference is that currently if we moved that comprehension into the function signature, it would use the `items` from the surrounding scope (because of early binding). It has to be set up in such a way that items comes from the correct scope too. If we were willing to give up fast locals, I think that the normal LEGB lookup will do the trick. That works for locals inside classes, so I expect it should work here too.
(b) guaranteed evaluation on function entry,
If that's the behaviour that people prefer, sure. Functions would need to know which parameters were: 1. defined with a lazy default; 2. and not passed an argument by the caller (i.e. actually using the default) and for that subset of parameters, evaluate them, before entering the body of the function. That's kinda what you already do, isn't it? One interesting feature here is that you don't have to compile the default expressions into the body of the function. You can stick them in the code object, as distinct, introspectable thunks with a useful repr. Potentially, the only extra code that needs go inside the function body is a single byte-code to instantiate the late-bound defaults. Even that might not need to go in the function body, it could be part of the CALL_FUNCTION and CALL_FUNCTION_KW op codes (or whatever we use).
(c) the ability to put it in the function header.
Well sure. But if we have syntax for a lazily evaluated expression it would be an expression, right? So we can put it anywhere an expression can go. Like parameter defaults in a function header. The point is, Rob thought (and possibly still does, for all I know) that lazy evaluation is completely orthogonal to late-bound defaults. The PEP makes that claim too, even though it is not correct. With a couple of tweaks that we have to do anyway, and perhaps a change of syntax (and maybe not even that!) we can get late-bound defaults *almost* for free if we had lazy evaluation. That suggests that the amount of work to get *both* is not that much more than the work needed to get just one. Why have a car that only drives to the mall on Thursdays when you can get a car that can drive anywhere, anytime, and use it to drive to the mall on Thursday as well?
Please stop arguing this point. It is a false analogy and until you can demonstrate *with code* that there is value in doing it, it is a massive red herring.
You can make further debate moot at any point by asking Python-Dev for a sponsor for your PEP as it stands right now. If you think your PEP is as strong as it can possibly be, you should do that. (You probably want to fix the broken ReST first.) Chris, you have been involved in the PEP process for long enough, as both a participant of discussions and writer of PEPs, that you know damn well that there is no requirement that all PEPs must have a working implementation before being accepted, let alone being *considered* by the community. Yes, we're all very impressed that you are a competent C programmer who can write an initial implementation of your preferred design. But your repeated gate-keeping efforts to shut down debate by wrongly insisting that only a working implementation may be discussed is completely out of line, and I think you know it. Being a C programmer with a working knowledge of the CPython internals is not, and never has been, a prerequisite for raising ideas here. I do feel some sympathy for you. I can't imagine the frustration you may be feeling -- your intent is a really tightly focused narrow feature, late-bound defaults. And not only do some people refuse to see what a fantastic idea it is, and keep raising weaknesses of the PEP itself, but some of them are more interested in a superset of the feature! But I also can't help but feel that some of this is self-inflicted. It looks to me that you jumped the gun on not only writing a PEP but an implementation as well, *long* before there was even close to a consensus on Python-Ideas (let alone in the broader community) that late-bound defaults are worth the additional syntax, or what their precise behaviour should be. And ever since then (it seems to me) you have been trying to shut down any debate that isn't narrowly focused on your design. Of course it is your right to code whatever you want, whenever you like. But you don't then get to complain about people wanting to consider a larger feature set when they never agreed that they wanted late-bound defaults alone in the first place.
Even if Python does later on grow a generalized lazy evaluation feature, it will only change the *implementation* of late-bound argument defaults, not their specification.
Great, we're in agreement that late-bound defaults can be implemented via lazy evaluation. You should fix the PEP that wrongly describes them as unrelated. -- Steve