
Hi Chris, I feel like we're pretty close to agreement. :-) The only difference is that I still lean toward allowing one of the two left-to-right options, and not trying to raise SyntaxErrors. I feel like detecting this kind of bad code belongs more with a linter than the programming language itself. But you're definitely right that it's easier to give permissions later than take them away, and there are two natural left-to-right orders... Speaking of implementation as Guido just raised, maybe going with what makes the most sense in the implementation would be fitting here? I'm guessing it's left-to-right overall (among all arguments), which is also the simpler-to-explain rule. I would actually find it pretty weird for references to arguments to the right to make sense even if they could... Actually, if we use the left-to-right overall order, this is the more conservative choice. If code worked with that order, and we later decided that the two-pass default assignment is better, it would be backward-compatible (except that some previously failing code would no longer fail). On Tue, 26 Oct 2021, Chris Angelico wrote:
Personally, I'd expect to use late-bound defaults almost all or all the time; [...]
Interesting. In many cases, the choice will be irrelevant, and early-bound is more efficient. There aren't many situations where early-bind semantics are going to be essential, but there will be huge numbers where late-bind semantics will be unnecessary.
Indeed; you could even view those cases as optimizations, and convert late-bound immutable constants into early-bound defaults. (This optimization would only be completely equivalent if we stick to a global left-to-right ordering, though.)
A key difference from the PEP is that JavaScript doesn't have the notion of "omitted arguments"; any omitted arguments are just passed in as `undefined`; so `f()` and `f(undefined)` always behave the same (triggering default argument behavior).
Except when it doesn't, and you have to use null instead... I have never understood those weird inconsistencies!
Heh, yes, it can get confusing. But in my experience, all of JavaScript's built-in features treat `undefined` as special; it's the initial value of variables, it's the value for omitted arguments; etc. `null` is just another sentinal value, often preferred by programmers perhaps because it's shorter and/or better known. Also, confusingly, `undefined == null`. Eh, and `null ?? 5` acts the same as `undefined ?? 5` -- never mind. :-)
There is a subtlety mentioned in the case of JavaScript, which is that the default value expressions are evaluated in their own scope:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/...
Yeah, well, JS scope is a weird mess of historical artifacts. Fortunately, we don't have to be compatible with it :)
That is true, but default values aren't part of the original history; they were added in ECMAscript 5 in 2009. So they probably had some issues in mind here, as it seems like added complexity, so was probably an intentional addition.
This is perhaps worth considering for the Python context. I'm not sure this is as important in Python, because UnboundLocalError exists (so attempts to access things in the function's scope will fail), but perhaps I'm missing a ramification...
Hmm. I think the only way it could possibly matter would be something like this:
def f(x=>spam): global spam spam += 1
Unsure what this should do. A naive interpretation would be this:
def f(x=None): if x is None: x = spam global spam spam += 1
and would bomb with SyntaxError. But perhaps it's better to permit this, on the understanding that a global statement anywhere in a function will apply to late-bound defaults; or alternatively, to evaluate the arguments in a separate scope. Or, which would be a simpler way of achieving the same thing: all name lookups inside function defaults come from the enclosing scope unless they are other arguments. But maybe that's unnecessarily complicated.
Inspired by your example, here's one that doesn't even involve `global`: ``` spam = 5 def f(x := spam): spam = 10 f() ``` Does this fail (UnboundLocalError or SyntaxError or whatever) or succeed with x set to 5? If we think of the default arguments getting evaluated in their own scope, is its parent scope the function's scope or its enclosing scope? The former is closer to the `if x is None` behavior we're replacing, while the latter is a bit closer to the current semantics of default arguments. I think this is very confusing code, so it's not particularly important to make either choice, but we need to make a decision. The less permissive thing seems to be using the function's scope (and fail), so perhaps that's a better choice. On the other hand, given that `global spam` and `nonlocal spam` would just be preventing `spam` from being defined in the function's scope, it seems more reasonable for your example to work, just like the following should: ``` spam = 5 def f(x := spam): print(x, spam) # 5 5 f() ``` Here's another example where it matters whether the default expressions are computed within their own scope: ``` def f(x := (y := 5)): print(x) # 5 print(y) # 5??? f() ``` I feel like we don't want to allow accessing `y` in the body of `f` here, because whether `y` is bound depends on whether `x` was passed. (If `x` is passed, `y` won't get assigned.) This would suggest evaluating default expressions in their own scope would be beneficial. Intuitively, the parens are indicating a separate scope, in the same way that `(x for x in it)` creates its own scope and thus doesn't leak `x`. On the other hand, `((y := x) for x in it)` does seem to leak `y`, so I'm not really sure what would be best / most consistent here. Erik -- Erik Demaine | edemaine@mit.edu | http://erikdemaine.org/