Exp HP
Exp HP

Reputation: 725

Create Proc with current value of variable

I've been having a problem with a pesky little function in a class in a library that I did not create (and thus, cannot edit). Here's a simple class with the annoying behavior isolated:

class Foo              # This is a class I cannot
  def setmyproc(&code) # safely edit.
    @prc = Proc.new    # Due to it being in a
  end                  # frustratingly complex
  def callmyproc()     # hierarchy, I don't
    @prc.call          # even know how to reopen
  end                  # it. Consider this class
end                    # set in stone.

I run into a problem when I try to iterate and generate an array of these objects. I expected a different value to be substituted for i into the Proc with each object, but what happens instead is that the variable i is shared between them.

$bar = []
for i in (0..15)
  $bar[i] = Foo.new
  $bar[i].setmyproc { puts i }
end

$bar[3].callmyproc # expected to print 3
$bar[6].callmyproc # expected to print 6

Output

  15
  15

What can I do inside the loop to preserve separate values of i for each object?

Upvotes: 2

Views: 349

Answers (3)

Matt Briggs
Matt Briggs

Reputation: 42158

Ok, so first of all, welcome to closures :)

A closure is a piece of code you can pass around like a variable, that is the easy part. The other side is that a closure maintains the scope that it was called in.

What is actually happening is that as you store your procs, each one is taking along a reference to n. even though you go out of the scope of the for loop, that reference to n still sticks around, and every time you execute your procs, they are printing the final value of n. The problem here is that each iteration is not in its own scope.

What Adrian suggested to do is swap your for loop for a range.each block. The difference is that each iteration does have its own scope, and that is what is bound to the proc

$bar = []
(0..15).each do |i|
  #each i in here is local for this block
  $bar[i] = Foo.new
  $bar[i].setmyproc { puts i }
end

This really is not a simple thing to wrap your head around, but its one of those things that will keep tripping you up until you really get it. I probably did a terrible job of explaining it, if it doesn't gel I would spend a bit of time googling how closures work with scope.

Upvotes: 1

Bryan Ash
Bryan Ash

Reputation: 4479

The block that gets passed into each Foo in the $bar array is bound to the same variable i. Anytime you send callmyproc the current value of i in the original scope is used.

$bar[3].callmyproc
=> 15
$bar[6].callmyproc
=> 15

i = 42

$bar[3].callmyproc
=> 42
$bar[6].callmyproc
=> 42

You need to send a different object into each proc:

0.upto(15) do |i|
  $bar[i] = Foo.new
  $bar[i].setmyproc { i.to_i }
end

$bar[3].callmyproc
 => 3 
$bar[6].callmyproc
 => 6 

Upvotes: 2

Adrian
Adrian

Reputation: 15171

Use this:

$bar = []
(0..15).each do |i|
  $bar[i] = Foo.new
  $bar[i].setmyproc { puts i }
end

$bar[3].callmyproc # prints 3
$bar[6].callmyproc # prints 6

If you really need to make the change inside of the loop, use this (ruby 1.9 only):

$bar = []
for i in (0..15)
  ->(x) do
    $bar[x] = Foo.new
    $bar[x].setmyproc { puts x }
  end.(i)
end

$bar[3].callmyproc # prints 3
$bar[6].callmyproc # prints 6

Upvotes: 3

Related Questions