In truth, I've never understood the fuss about `let`. It's taken a huge amount of Hy development effort, but it adds very little. Python's native scoping works well in the large majority of cases, and in tricky cases, I'd rather use `nonlocal` or `global` explicitly so I'm sure what's happening at the level of Python semantics. I suspect that people who heard that previous versions of Hy were missing `let` assumed that some other feature (such as nested lexical scopes) was also missing that has actually always been there.
(let [a 1]
(defn foo []
(setv a 2))
(foo)
(print a))
you probably `2` to be printed, but the seeming equivalent Python code a = 1
def foo():
a = 2
foo()
print(a)
prints `1`. Python interprets the assignment as declaring a new local variable. Frankly, Python got scoping wrong (in its quest to eliminate declarations), but here we are. So `let` needs to work around this somehow. The current implementation implicitly adds `global a` or `nonlocal a` as appropriate. (Actually, the name `a` is also mangled so that any references to `a` outside the `let` refer to a different variable.) a = 1
def foo():
x = a
foo()
so if this 'a' is a new variable, what happens when the name collides with a global variable? a = 1
def foo():
a = a
foo()
> UnboundLocalError: local variable 'a' referenced before assignment
yuck!Python is generally quite good at having a verbose option to express things, so I'm surprised there's not an explicit way to reference the enclosing scope from an object.
a = 1
def foo():
x = a
a = 2
foo()
> UnboundLocalError: local variable 'a' referenced before assignment
Basically a name cannot be both global and local inside the same function. Otherwise it would be a total mess."a = a" just means assign the value of local variable "a" to a local variable "a", so the error makes sense.
>>> a = 1
>>> def foo():
... a = globals()['a']
... print(a)
...
>>> foo()
1
Still yuck? >>> def foo():
... import __main__
... a = __main__.a
... print(a)
...
>>> foo()
1But maybe it is because I am familiar with Clojure (where `setv` or equivalents doesn't exist) and not other Lisps.
This would be the equivalent Clojure code anyway:
user=> (let [a 1] (defn foo [] (def a 2)) (foo) (println a))
1
nil
So yeah, makes sense for me. But can understand why some other people would expect the other way though. * (let ((a 1))
(defun foo ()
(setq a 2))
(foo)
(print a))
2
2 def f():
a = 1
def g():
nonlocal a
a = 2
g()
print(a)
though nonlocal can't be used when referring to global variables... it's kinda bad.My use case is Jupyter notebooks. With normal Python scoping you quickly accumulate a lot of global variables and you might accidentally use the wrong one. With let-bindings you have variables that clean up after themselves.