Ok, let’s step back a bit … making these rules dependent on global context – whether a variable exists already or not – sounds like a recipe for confusion.
Here are some arguments, why I think it might not be a good idea in the first place:
-
Usually functions are bound globally and (as you acknowledge) your rule would apply to functions too. This would be very annoying for interactive work, i.e., define g, define f using g, test f and see its wrong (problem was in g already), fix g, try f again (success).
Note that with your rule, the old definition of g would still stick around and f would need to be redefined as well to see the change – as you can imagine this gets messy very quickly with any kind of interactive workflow. If you ever worked with Python it is annoying enough that old objects stick around when you redefine a class and languages designed for interactive use, e.g., Smalltalk, Common Lisp, can even handle this case gracefully.
Some compiled languages could get away with this, e.g., Haskell which only allows a single global definition anyways. -
Your rule would also not solve the fundamental problem of “spooky action at a distance” due to global variables but simply shift the problem:
glob_a = 10 # Lot's of other important stuff f(x, y) = x * glob_a + y # Some more scrolling here glob_a = 20 # Further down the lines f(2, 3) # What will/should this be?
Local reasoning does not work in any case …
-
Finally, it would fundamentally change the semantic of functions which don’t evaluate their body until called (part of it would be evaluated by your rule already at definition time). You’re right that closures do keep a reference to their defining environment, but this was actually introduced to restore local reasoning for higher-order functions:
function adder(x) fun(y) = x + y # x is free here fun end x = 10 a1 = adder(1) a1(x) # 11
You can try that this breaks in Emacs Lisp being one of the few languages still around with dynamic binding (modern language do provide dynamic variables as an option though, including Julia):
(defun adder(x) (lambda (y) (+ x y))) (setq x 10) (setq a1 (adder x)) (funcall a1 x) // 20
Dynamic variables – or simply redefining a global variable – can be very handy for (locally) changing configuration options
somefile = open(...) redirect_stdout(somefile) do print("Hallo") end
which would not be possible when following your rule.