Let is rather integral part of lisps as it is used to define lexical scope for a one or more symbols and bind values to them. However, since version 0.13.0 Hy doesn’t have one. Lets have a look what lead to this decision.
We have to remember that Hy is first and foremost just a clever preprocessor that translates lisp syntax into Python AST. Therefore we’re bound by the rules of Python, albeit we can sometimes try to work around them.
In order to examine what happens under the hood, we launch Hy REPL with –spy switch. This causes REPL to show to what kind of Python code our Hy code was translated, before executing it:
$ hy --spy hy 0.12.1 using CPython(default) 3.4.3 on Linux => (+ 1 2 3) ((1 + 2) + 3) 6 =>
Like I mentioned earlier, Hy is bound by the rules of Python. Hy translates let to a function that gets called immediately:
=> (let [a 1 b 2] ... (+ a b)) def _hy_anon_fn_1(): a = 1 b = 2 (a, b) return (a + b) _hy_anon_fn_1() 3
This is a handy trick that ensures that even if a or b had been defined before, they would have new values inside the let and any changes made there wouldn’t affect the original values:
=> (let [a 1 b 2] ... (let [a 2 c 3] ... (+ a b c))) def _hy_anon_fn_2(): a = 1 b = 2 (a, b) def _hy_anon_fn_1(): a = 2 c = 3 (a, c) return ((a + b) + c) return _hy_anon_fn_1() _hy_anon_fn_2() 7
Here lies many subtle and not so subtle obstacles that one needs to navigate in case they would want to use this system. They tended to pop up now and then, raise lots of confusion and require debugging and work.
For example, if you had a variable outside of let block that you wanted to mutate while inside of the let block, you had to remember that there were invisible function in the play. Otherwise you ended up modifying wrong variable:
=> (defn test  ... (setv a 1) ... (let [b 2] ... (setv a b)) ... a) def test(): a = 1 def _hy_anon_fn_1(): b = 2 a = b return a _hy_anon_fn_1() return a => (test) test() 1
In the previous example, there are two symbols name to a, the outer and the inner. Changes to the inner one aren’t reflected on the outer one and thus incorrect value of 1 is returned instead of the expected 2. The solution to this problem is to use nonlocal:
=> (defn test  ... (setv a 1) ... (let [b 2] ... (nonlocal a) ... (setv a b)) ... a) def test(): a = 1 def _hy_anon_fn_1(): b = 2 nonlocal a a = b return a _hy_anon_fn_1() return a => (test) test() 2
Hy can’t put the nonlocal there by default, because then there wouldn’t be much point in having let blocks to begin with. Programmer has to know about the hidden function and plan their code around this fact.
Another error (as shown in issue 1212) happened when symbol being bound in let had same name as symbol in the outer scope:
=> (defn test [x] ... (let [x (inc x)] ... (print x))) from hy.core.language import inc def test(x): def _hy_anon_fn_1(): x = inc(x) return print(x) return _hy_anon_fn_1() => (test 2) test(2) Traceback (most recent call last): File "/usr/lib/python3.4/code.py", line 90, in runcode exec(code, self.locals) File "<input>", line 1, in <module> File "<input>", line 1, in test File "<input>", line 2, in _hy_anon_fn_1 UnboundLocalError: local variable 'x' referenced before assignment
The fix suggested in the GitHub issue would have fixed this error, but instead of applying yet another patch over the sore point, let was completely removed.
Since let introduces an invisible function, placing one inside a for loop and breaking out is asking for trouble as the break keyword is allowed only inside of a loop. The following example demonstrates this:
=> (for [x (range 10)] ... (let [res (+ x 1)] ... (if (> x 5) ... (break) ... (print res)))) from hy.core.language import range for x in range(10): def _hy_anon_fn_1(): res = (x + 1) if (x > 5): break _hy_anon_var_1 = None else: _hy_anon_var_1 = print(res) return _hy_anon_var_1 _hy_anon_fn_1() Traceback (most recent call last): ... File "<input>", line 1 SyntaxError: 'break' outside loop
The easiest way to fix this is to get rid of let (but then there isn’t local scope anymore of course):
=> (for [x (range 10)] ... (setv res (+ x 1)) ... (if (> x 5) ... (break) ... (print res))) from hy.core.language import range for x in range(10): res = (x + 1) if (x > 5): break _hy_anon_var_1 = None else: _hy_anon_var_1 = print(res) 1 2 3 4 5 6
Even more obscure bug was highlighted in issue 619. Essentially, the invisible function created by let can lead to infinite loop when combined with while and yield. First the incorrect version:
=> (defn test  ... (while True ... (let [foo "bar"] ... (yield foo)))) def test(): while True: def _hy_anon_fn_1(): foo = 'bar' return yield foo _hy_anon_fn_1() => (test) test() ^CTraceback (most recent call last): File "/usr/lib/python3.4/code.py", line 90, in runcode exec(code, self.locals) File "<input>", line 1, in <module> File "<input>", line 257, in test KeyboardInterrupt
As you can see, the resulting Python code has an infinite loop and inside of the loop there’s a new function (introduced by let) and inside of that there’s the yield that the programmer expected to yield “bar”. But because the yield is inside of the function that gets called as the last step of the while loop, the resulting generator object doesn’t return execution outside of while. Instead a new generator object is created indefinitely. One way to fix this is to reorder the code:
=> (defn test  ... (let [foo "bar"] ... (while True ... (yield foo)))) def test(): def _hy_anon_fn_1(): foo = 'bar' while True: yield foo return _hy_anon_fn_1() => (test) test() <generator object _hy_anon_fn_1 at 0xb663fa54>
While this works, it’s not exactly the same end result. What if the let block contained more logic and can foo should be evaluated each time before yielding it? That behaviour is not possible with the latter code.
Most likely I forgot some edge or corner cases with let in this blog posting and most likely we didn’t even saw them all. The end result was in any case that the let was removed because it was causing trouble all the time and nobody managed to come up with a reasonable solution that would have fixed all the known issues.