I am not sure if this is a bug or intended behavior...
vim -Nu NONEvim9script
for [idx, val] in "hello world!"->items()
timer_start(idx * 1000, (_) => {
echow $"{idx}: {val}"
})
endfor
sourceI would expect message window to show every letter of hello world with an index of a letter.
Instead, I can see the last char and no indices at all (plus if you do visual mode while timer is active, -- VISUAL -- appears in message window:

Looks like lambda in timer_start doesn't "enclosure" outer variables defined in for loop.
I was assuming it should work, looking to the help example:
This can be useful for a timer, for example: >
var count = 0
var timer = timer_start(500, (_) => {
count += 1
echom 'Handler called ' .. count
}, {repeat: 3})
9.0.0420
No response
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
If you put the whole for loop into function, it shows the latest idx and val:
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
And as a side note, :echow for sure has an issue with the visual line mode:
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think closure part works fine in the variant of a function -- all timers use the same idx and val that by the time of execution have the latest values of the items().
So it is just inconsistent with the plain for loop on the script level.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
I think closure part works fine in the variant of a function -- all timers use the same idx and val that by the time of execution have the latest values of the items().
I think there is a bug, because according to the help (somewhere around :help E1271), that should work:
def GetClosure(idx: number, val: string): func
return () => {
echowindow $'{idx}: {val}'
}
enddef
for [idx, val] in 'hello world!'->items()
timer_start(idx * 1'000, (_) => {
GetClosure(idx, val)()
})
endfor
But it does not. idx is not printed, and val is always the last character (!).
Something else is weird, if we try to capture idx and val in a global variable, we can't print its value:
vim9script def GetClosure(idx: number, val: string): func g:var = get(g:, 'var', []) + [[idx, val]] return () => { echowindow $'{idx}: {val}' } enddef
for [idx, val] in 'hello world!'->items
()
timer_start(idx * 1'000, (_) => {
GetClosure(idx, val)()
})
endfor
echo g:varThe last :echo does not print anything. Even g:var->string() does not print anything. However, json_encode() does print something:
[[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"],[,"!"]]
Notice that in each nested list, the first idx item is missing:
[,"!"]
^
✘
That looks wrong.
There is also a crash:
vim9script for n in [0] timer_start(0, (_) => { echo n }) endfor
And as a side note, :echow for sure has an issue with the visual line mode:
Yes, I don't think the "-- VISUAL LINE --" message should also be printed inside the window.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you are subscribed to this thread.![]()
I fixed the problem with putting the mode message in the message window and the crash.
When a closure is defined it remembers variables used from its context. These variables are then accessed at the time the closure is executed. Thus you get the latest value, not the value from when the closure was defined.
I understand that in a loop you may want to use the current value, not a reference to the variable. I don't know a simple solution for this. I wonder how this is done in other languages.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
according to the help (somewhere around :help E1271), that should work:
I was wrong. This code can't work as expected because GetClosure() is called too late.
OTOH, this works as expected:
vim9script def GetClosure(idx: number, val: string): func
return (_) => {
echowindow $'{idx}: {val}'
}
enddef
for [idx, val] in 'hello world!'->items
()
timer_start(idx * 1'000, GetClosure(idx, val))
endfor0: h
1: e
2: l
3: l
4: o
5:
6: w
7: o
8: r
9: l
10: d
11: !
The help which explains how to use function calls to create separate contexts could be improved a little. Just a missing comma:
diff --git a/runtime/doc/vim9.txt b/runtime/doc/vim9.txt index aba0f0051..9939555b5 100644 --- a/runtime/doc/vim9.txt +++ b/runtime/doc/vim9.txt @@ -1311,7 +1311,7 @@ Make sure to define the breakpoint before compiling the outer function. The "inloop" variable will exist only once, all closures put in the list refer to the same instance, which in the end will have the value 4. This is efficient, also when looping many times. If you do want a separate context -for each closure call a function to define it: > +for each closure, call a function to define it: > def GetClosure(i: number): func var infunc = i return () => infunc
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
OTOH, this works as expected:
It works because the lambda is no longer created directly inside the loop, but from a function. Vim uses the same context only in the former case (for better performance); not in the latter. IOW, the current design privileges performance.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Now with patches if I do the same as described in first message:
vim9script
for [idx, val] in "hello world!"->items()
timer_start(idx * 1000, (_) => {
echow $"{idx}: {val}"
})
endfor
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
And apparently you can't pre-define those variables to avoid deletion:
vim9script
var idx: number
var val: string
for [idx, val] in "hello world!"->items()
timer_start(idx * 1000, (_) => {
echow $"{idx}: {val}"
})
endfor
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
When a closure is defined it remembers variables used from its context. These variables are then accessed at the time the closure is executed. Thus you get the latest value, not the value from when the closure was defined.
Indeed, it makes sense. Thank you!
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Now with patches if I do the same as described in first message:
I think this is working as intended (at least for now). idx and val are defined in a block at the script-level. They live as long as the block is being processed. As soon as the execution leaves the block, they are deleted. When the timers' callbacks are executed, the execution has left the block. It can't find the variables, hence the error.
And apparently you can't pre-define those variables to avoid deletion:
We can't shadow variables. That is, the same name can't be declared in 2 different scopes. If idx is declared in the script-local scope, then it can't be declared in the block-local scope too (for idx in ... declares idx implicitly). If that was allowed, then we wouldn't be able to access the idx declared in the script-local scope from the block-local one. It's not fundamentally impossible, but it was chosen to be disallowed, presumably to avoid writing confusing code (FWIW, after having read/written too much code which is hard to debug because of poorly named variables, I tend to agree).
Here is the current solution to the issue:
vim9script def GetClosure(idx: number, val: string): func return (_) => { echowindow $'{idx}: {val}' } enddef
for [idx, val] in 'hello world!'->items
()
timer_start(idx * 1'000, GetClosure(idx, val)) endfor
0: h
1: e
2: l
3: l
4: o
5:
6: w
7: o
8: r
9: l
10: d
11: !
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Closed #11094 as completed.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Thx, @lacygoill, I get it that a new context has to be created with a separate function.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
If you want it more compact, use a lambda and call it immediately within the timer function:
for [idx, val] in 'hello world!'->items
()
timer_start(idx * 1000, ((i: number, v: string) => (_) => {
echowindow $'{i}: {v}'
})(idx, val))
endforOr slightly more readable:
for [idx, val] in 'hello world!'->items
()
timer_start(idx * 1000, (i: number, v: string) => {
return (_) => {
echowindow $'{i}: {v}'
}
}(idx, val))
endfor—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
You can currently achieve this by creating a new block scope as well but I think it really should work like this without the extra scope.
for [idx, val] in "hello world!"->items
()
{
var i = idx
var v = val
timer_start(idx * 1000, (_) =
> {
echow $"{i}: {v}"
})
}
endforIt seems to me there are currently two surprising features of the implementation, assuming I understand correctly:
Ignoring any implementation difficulties I think these semantics are confusing.
The scope of the loop variable is less important but generally these for loops are desugared to either (using the example from :help E1271):
var list = range(5) var flist: list<func> { var val: number var i = 0 while i < len(list) val = list[i] flist[i] = () => val i = i + 1 endwhile } echo range(5)->map((j, _) => flist[j]()) # => [4, 4, 4, 4, 4]
var list = range(5) var flist: list<func> var i = 0 while i < len(list) { var val = list[i] flist[i] = () => val i = i + 1 } endwhile echo range(5)->map((j, _) => flist[j]()) # => [0, 1, 2, 3, 4]
I think the latter form is becoming more popular as it's become clear that it's what most users expect. C# made a breaking change to modify the behaviour of their foreach loop from the first to the second form some years ago and in JavaScript it was considered a gotcha until ES6.
Amongst some of the other languages you've surveyed when designing Vim9, Dart, Java and Lua use the second form, while Go and Ruby use the first.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Still wondering if there is a simpler solution we can borrow from another language.
This is what the official Python documentation recommends: Why do lambdas defined in a loop with different values all return the same result?
In C++ it is possible to capture by reference or by copy. More detailed explanation can be found on cppreference.
Here's an example where the variable is captured by copy:
#include <cstdio> #include <functional> #include <vector> int main() { std::vector<std::function<int()>> squares; for (int i = 0; i < 5; i++) { // capture variable 'i' by copy squares.push_back([i](){ return i * i; }); } for (int i = 0; i < 5; i++) { printf("%d ", squares[i]()); // output: 0 1 4 9 16 } }
Same as above but capture variable by reference:
#include <cstdio> #include <functional> #include <vector> int main() { std::vector<std::function<int()>> squares; for (int i = 0; i < 5; i++) { // capture variable 'i' by reference squares.push_back([&i](){ return i * i; }); } for (int i = 0; i < 5; i++) { printf("%d ", squares[i]()); // output: 25 25 25 25 25 } }
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
The following example throws an error at script level but works fine inside a def function:
var squares: list<func(): number> = [] for x in range(5) squares->add(() => x * x) endfor for F in squares echo F() endfor
Result:
Error detected while processing /tmp/test.vim[43]..function <lambda>16:
line 1:
E1302: Script variable was deleted
Doing the same inside a function works:
vim9script def Test() var squares: list<func(): number> = [] for x in range(5) squares->add(() => x * x) endfor for F in squares echo F() endfor enddef Test()
Result:
16
16
16
16
16
I would have expected the same behavior.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
The following two identical code snippets (one at script level, the other one in a def function) give two different results.
First example, code is executed at script level:
var squares: list<func> = [] for n in range(5) { const nr = n squares[nr] = () => nr * nr }
endfor for F in squares echo F() endfor
Output: 0 1 4 9 16
Second example, code is executed with a def function:
def Test() var squares: list<func> = [] for n in range(5) { const nr = n squares[nr] = () => nr * nr }
endfor for F in squares echo F() endfor enddef Test()
Output: 16 16 16 16 16
Should I open a separate issue for this?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
For some reasons Bram's message didn't make it to github.
Here's the link: https://groups.google.com/g/vim_dev/c/gTJ2N8A-txQ/m/ML6wYx5cAQAJ
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
The following example throws an error at script level but works fine inside a def function:
This has been fixed by patches 9.0.0459 and 9.0.0460.
The following two identical code snippets (one at script level, the other one in a def function) give two different results.
They still give different results.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
It now also works in compiled code. Please watch out for any problems or memory leaks.
I'm not sure yet what to do with nested loops. Currently only variables declared in the loop block itself are copied.
Is there a good example of what doesn't work but should work?
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
And now it also works in compiled code for nested loops. This was complicated!
Most of it implemented with patch 9.0.0502 (you also need the next two).
I hope it doesn't slow down normal loops. And hopefully this hasn't introduced memory leaks.
Let me know if you still spot a problem.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
It seems the for [{var1}, {var2}, ...] in {listlist} is currently using a different scope for {var1} than for the rest of the variables when executed in a script. It works as expected when wrapped in a compiled function.
So the original example from this report is printing:
11: h
11: e
...
11: d
11: !
rather than
11: !
...
11: !
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Right, it miscomputes the location of the second loop variable and assumes it's declared inside the block.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Thanks for working on this @brammool - looks good now.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
@brammool, are you quoting the right message here? The examples you quoted now both work correctly without error, producing the same result.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
However, in the next and final report from @bfrg, with the now unnecessary extra {...} block in each of the for loops, the two examples still give different results.
The following two identical code snippets (one at script level, the other one in a
deffunction) give two different results.
First example, code is executed at script level:
vim9script
var squares: list<func> = [] for n in range(5) { const nr = n squares[nr] = () => nr * nr }
endfor for F in squares echo F() endfor
Output:
0 1 4 9 16
Second example, code is executed with a
deffunction:
vim9script def Test() var squares: list<func> = [] for n in range(5) { const nr = n
squares[nr] = () => nr *
nr } endfor
for F in squares echo F() endfor enddef Test()
Output:
16 16 16 16 16
If you remove the unnecessary {...} block then the result is the same as the first example.
The extra block has, correctly, no effect in the first example.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
Personally, I think vimscript should behave the same inside a def function and on a script level.
Here's another simple for-loop example (without any closures):
vim9script # Filter out all odd numbers in each sublist var list: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] for i in list filter(i, (_, n: number): bool => n % 2 == 0) endfor # [[], [2], [2], [2, 4], [2, 4]] echo list
Everything works as expected. However, inside a def function, the above snippet throws an error:
vim9script def Test() var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] for i in items filter(i, (_, n: number): bool => n % 2 == 0) endfor echo items enddef Test()
Result:
Error detected while compiling /tmp/test.vim[34]..function <SNR>22_Test:
line 3:
E1307: Argument 1: Trying to modify a const list<number>
What does this mean? Does vim copy each item in list inside the for-loop? That would be quite inefficient. The items could be anything.
The following works but looks very complicated:
def Test() var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] for i in items { var item: list<number> = i filter(item, (_, n: number): bool => n % 2 == 0) } endfor echo items enddef
I feel like the old way was more intuitive where the for-loop variable was a reference to the current item in the list.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
The following works but looks very complicated:
def Test() var items: list<list<number>> = [[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]] for i in items { var item: list<number> = i filter(item, (_, n: number): bool => n % 2 == 0) } endfor echo items enddef
That doesn't need the extra {...} block in the for loop.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
The Go people seem to agree with the C# people that this is confusing enough to users that it's worth introducing a breaking change. I don't feel strongly about it one way or the other but apparently I'm in the minority or just plain wrong.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
That doesn't need the extra {...} block in the for loop.
Thanks, I didn't notice that.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()
This issue is getting complicated, and it actually is already closed. Please create a separate issue for each problem.
—
Reply to this email directly, view it on GitHub.
You are receiving this because you are subscribed to this thread.![]()