Confused about closures

This code exemplifies something I don’t understand about Crystal:

counter = 0
procs = [] of -> Nil

5.times do |time|
  procs << ->{ puts({counter, time}) }
  counter += 1
end

procs.each &.call

Output:

{5, 0}
{5, 1}
{5, 2}
{5, 3}
{5, 4}

I expected counter’s value to go from 0 to 4 inside each proc. Integers are value types, so I don’t expect counter += 1, or any other operation, to be able to change the value of another integer. But each proc prints 5, so it appears that they access the integer by reference somehow.

Maybe this is due to this:

In this case the compiler allocates x on the heap and uses it as the context data of the proc to make it work, because normally local variables live in the stack and are gone after a method returns.

https://crystal-lang.org/reference/1.7/syntax_and_semantics/closures.html

But, personally, I still expected value types to be “duplicated” into closure contexts. Which seems to be what’s happening with time! Why the difference?

And lastly, is there some trick to get the behavior I want? As in, each proc capturing a copy of counter.

I think that’s exactly right. each proc closures the same pointer to the counter variable, such that by the time they are called, that variable is now equal to 5. So they all print 5. While not directly related, this scenario is a lot like Concurrency - Crystal. Which shows that you can get the behavior you want by creating a proc and calling it such that a copy of the integer is created:

counter = 0
procs = [] of -> Nil
 
5.times do |time|
  procs << ->(c : Int32) do
    ->{ puts({c, time}) }
  end.call counter
  counter += 1
end
 
procs.each &.call
{0, 0}
{1, 1}
{2, 2}
{3, 3}
{4, 4}
1 Like

That cause by the counter variable closure by the Proc, you can fix it with assign it to a new variable, like this:

counter = 0
procs = [] of -> Nil

5.times do |time|
  _counter = counter
  procs << ->{ puts({_counter, time}) }
  counter += 1
end

procs.each &.call
{0, 0}
{1, 1}
{2, 2}
{3, 3}
{4, 4}
3 Likes

If the variable was frozen to the value it has at the time the proc literal is created, it wouldn’t be a closure. Keeping a reference and resolving it only when the code actually executes is exactly what closures are doing.

counter is declared outside the loop and thus each iteration of the loop uses the same variable. Refering to it in a proc body creates a closure. As @zw963 mentioned, you can avoid a closure by assigning the value to a new variable.
time is only declared inside the loop, thus it does not persist. Each iteration of the loop uses a fresh variable (which just happens to have the same name). It’s never reassigned so there’s no need to actually closure it. But it would be closured as well if you modify it inside the loop:

counter = 0
procs = [] of -> Nil

5.times do |time|
  _counter = counter
  procs << ->{ puts({_counter, time}) }
  counter += 1
  time += 10
end

procs.each &.call
{5, 10}
{5, 11}
{5, 12}
{5, 13}
{5, 14}
2 Likes

Thanks for the replies everyone!