tags:

views:

51

answers:

3

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?

+2  A: 

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
Adrian
Good thing I use Ruby 1.9. Is the -> syntax the only way to set up a temporary scope for a variable on demand?I'm quite surprised that the iterator in a for loop isn't limited to the loop in scope. That's unlike every other programming language I know (except for TI-BASIC).
Exp HP
Yes, that is one use for it. It is meant to create procs. `-> { ... }` is the same thing as `Proc.new { ... }`. It is not the only way to set up a temporary scope though. There are lots of ways, like this: `class << Object.new; ...; end`. `Object.new` will be garbage collected, and so the scope will be temporary.
Adrian
-> is the new syntax for a lambda (also known as the stabby lambda). the problem is not that there is no scope for the for loop, the problem is that there is no scope for each _iteration_ of the for loop (people who don't get closures run into literally this exact problem in every language that supports them that I have ever used). as an aside, rubyists very very rarely use for loops. check out each_with_index if you need a loop with an iteration number
Matt Briggs
@Matt Briggs about the scope being for each iteration: That is, of course, what I meant.
Exp HP
@exp: The way for loops usually work is "initialize i, increment at the end of each iteration" the way that it needs to work for a closure inside to bind the way you would expect it to is "each iteration create a new variable with the number of iterations, and pass it in to the loop". I don't know of any language that works that way.
Matt Briggs
@Briggs: Not quite! Because for loops in Ruby require an "in" clause, for loops in Ruby are moreso equivalent to foreach loops in other languages. That's what's throwing me off, because in most other languages, foreach loops always have a separate variable used in each iteration, and thus a scope confined to each iteration.
Exp HP
+2  A: 

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 
Bryan Ash
+1  A: 

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.

Matt Briggs
So what you're saying is a little contradictory to how I interpreted Adrian's solution, because I actually thought do() had its own scope for the variables declared in the pipes. Are you saying that objects are passed to do() "by reference" (to use the terminology of another language)? What I mean is, does it work only because of the limited scope of each iteration of each()/upto(), not because it's done within the confines of a do()?
Exp HP
Thats exactly it. It's not so much about reference vs value types, its that the value that comes through the pipes is created every iteration and goes away right after (from a scope perspective), as opposed to a for, where the same variable sticks around for the full execution of the loop.
Matt Briggs
I guess with Ruby I should stop trying to think of things in terms of scope and instead start thinking about where exactly each object I use comes from. Thanks for your help.
Exp HP
This is actually a closure thing, not a ruby thing. Understanding what the functional difference is between for loops and each blocks is important, but you could port this to C#, or Javascript, or Python and run into the exact same thing
Matt Briggs