flexxui / pscript

Python to JavaScript compiler
http://pscript.readthedocs.io
BSD 2-Clause "Simplified" License
256 stars 25 forks source link

Scoping / closures and callbacks? #52

Closed ed2050 closed 3 years ago

ed2050 commented 3 years ago

Looks like I hit my first major problem. Pscript scoping doesn't seem to work correctly with callbacks.

Run this code:

    for x in [1, 2] :
        window.setTimeout (lambda : log (f'got {x}'), 100)

In python this should print:

got 1
got 2

But in pscript it prints:

got 2
got 2

i.e. the lambda closure isn't set correctly. Pscript seems to be saving the last value of x and giving it to both function calls.

It's even worse if you use range (1,3) instead of the list above. Then it prints "got 3" twice. x should never be 3 in the expression "for x in range (1, 3)". Apparently pscript is incrementing x to the out-of-bounds value at the end of the loop, then giving it to any callers that use x.

I suppose this is just the normal headache javascript developers have to put up with. Not sure how scope & closures work in js.

Any suggestions how to work around this? Thanks.

almarklein commented 3 years ago
import asyncio

loop = asyncio.get_event_loop()

async def test():
    for x in [1, 2] :
        loop.call_later(0.1, lambda : print(f'got {x}'))

loop.run_until_complete(test())

gives:

got 2
got 2

I think both cases are correct - the variable x is in the scope that also contains the for-loop, but the body of a for-loop does not have its own scope for each iteration.

ed2050 commented 3 years ago

Thanks for the explanation Almar, it really helps! The lambda saves the reference not the value, even for primitive types.

So the behavior is correct, just surprising (at least to me). Closures are a mindbender. I probably shouldn't be using them after dark. :)

Anyway I found a workaround if anyone is interested. An extra level of closure does the trick. Output below is "got 3 got 4".

    for x in [3, 4] :
        def wrapper (y) :
            return lambda : log ('got %s' % y)
        func = wrapper (x)
        window.setTimeout (func, x * 100)
almarklein commented 3 years ago

The lambda saves the reference not the value, even for primitive types.

I think it saves a reference to the outer scope (or some sort of aggregate of that).

Another possible solution: window.setTimeout (lambda local_x: log (f'got {local_x}'), 100, x)