kuroko-lang / kuroko

Dialect of Python with explicit variable declaration and block scoping, with a lightweight and easy-to-embed bytecode compiler and interpreter.
https://kuroko-lang.github.io/
MIT License
427 stars 25 forks source link

[scope] `for` iteration scope #33

Closed anzhi0708 closed 1 year ago

anzhi0708 commented 1 year ago
>>> let ls = []
>>> for i in range(10):
  >     ls.append(lambda: i)
  >
>>> ls[0]()
 => 9
>>> ls[1]()
 => 9
>>> ls[2]()
 => 9
>>> print(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: Undefined variable 'i'.
>>>

In Python, for loops define a global variable which does not get cleaned after iterating. After Python's iteration, every i inside of lambda bodies points to the same value.

In Kuroko, i seems to be a local variable instead of a global one, but every i still got the same value - It works just like Python, which is not a bad thing, but I was expecting every for loop got its own scope / namespace / whatever - is this a feature in Kuroko too?

anzhi0708 commented 1 year ago

But in Kuroko I can use let to declare a local variable inside of for loop - which is great ;-)

klange commented 1 year ago

This capturing behavior is a tricky one. The scope of the implicitly declared loop variable is the loop as a whole, rather than an individual iteration of the body, so while the variable's scope does not extend after the loop as it would with the function scoping in Python, it does still exhibit the same capturing behavior where each iteration sees the same final variable rather than unique instances.

The reason for this specific scoping decision is that it more closely matches for-loop declarations in languages like C99 (which Kuroko's scoping rules are generally modeled on), and in fact Kuroko has 'legacy' support for C-style for loops in addition to the for ... in ... loops, which demonstrates the mapping pretty well:

for i=0; i<10; i++:
   print(i)

(Those might go away at some point, though, so please don't use them - I don't think they're any faster than in range(...) anyway...)

As you note, because we can force a new local in the body, we can provide different captured variables to the lambda in each iteration by forcing an extra assignment - a bit ugly, but quite useful! There's an instance of this in the documentation generator - instead of unpacking in the for, the unpack is done in the body to ensure separately-captured locals in lambdas.

Meshing Python's syntax with block scoping has been a challenge, and Kuroko definitely has some unfortunate quirks like this because of it, but sometimes those quirks have hidden benefits.

anzhi0708 commented 1 year ago

Thank you very much.