views:

72

answers:

3

While trying to brush up my Ruby skills I keep running across this case which I can't figure out an explanation for by just reading the API docs. An explanation would be greatly appreciated. Here's the example code:

for name in [ :new, :create, :destroy ]
  define_method("test_#{name}") do
    puts name
  end
end

What I want/expect to happen is that the name variable will be bound to the block given to define_method and that when #test_new is called it will output "new". Instead each defined method outputs "destroy" -- the last value assigned to the name variable. What am I misunderstanding about define_method and its blocks? Thanks!

+1  A: 

Blocks in Ruby are closures: the block you pass to define_method captures the variable name itself–not its value—so that it remains in scope whenever that block is called. That's the first piece of the puzzle.

The second piece is that the method defined by define_method is the block itself. Basically, it converts a Proc object (the block passed to it) into a Method object, and binds it to the receiver.

So what you end up with is a method that has captured (is closed over) the variable name, which by the time your loop completes is set to :destroy.

Addition: The for ... in construction actually creates a new local variable, which the corresponding [ ... ].each {|name| ... } construction would not do. That is, your for ... in loop is equivalent to the following (in Ruby 1.8 anyway):

name = nil
[ :new, :create, :destroy ].each do |name|
  define_method("test_#{name}") do
    puts name
  end
end
name # => :destroy
Kevin
I think I get it now. It looks like you found the source of the confusion, although without explicitly naming it: the problem is not so much that the block passed to `define_method` is a closure (in fact, that is *precisely* what the OP wants), but rather that the body of a `for` expression is *not* a closure. A `for` expression creates a new *local* variable *within* the scope the `for` expression is defined in. The `each` iterator OTOH takes a block, which is of course a closure, which means that now `name` refers to the specific instance of the `name` block variable *for that one iteration*
Jörg W Mittag
Oh, true. And actually, the variable created by the `for ... in` expression is still in scope *after* the loop, not just in the body of the loop (as I'm testing it in `irb`).
Kevin
A: 

The problem here is that for loop expressions do not create a new scope. The only things that create new scopes in Ruby are script bodies, module bodies, class bodies, method bodies and blocks.

If you actually look up the behavior of for loop expressions in the Draft ISO Ruby Specification, you will find that a for loop expression gets executed exactly like an each iterator except for the fact that it does not create a new scope.

No Rubyist would ever use a for loop, anyway: they would use an iterator instead, which does take a block and thus creates a new scope.

If you use an idiomatic iterator, everything works as expected:

class Object
  %w[new create destroy].each do |name|
    define_method "test_#{name}" do
      puts name
    end
  end
end

require 'test/unit'
require 'stringio'
class TestDynamicMethods < Test::Unit::TestCase
  def setup; @old_stdout, $> = $>, (@fake_logdest = StringIO.new) end
  def teardown; $> = @old_stdout end

  def test_that_the_test_create_method_prints_create
    Object.new.test_create
    assert_equal "create\n", @fake_logdest.string
  end
  def test_that_the_test_destroy_method_prints_destroy
    Object.new.test_destroy
    assert_equal "destroy\n", @fake_logdest.string
  end
  def test_that_the_test_new_method_prints_new
    Object.new.test_new
    assert_equal "new\n", @fake_logdest.string
  end
end
Jörg W Mittag
+1  A: 
for name in [ :new, :create, :destroy ]
  local_name = name
  define_method("test_#{local_name}") do
    puts local_name
  end
end

This method will behave as you expect. The reason for the confusion is that 'name' is not created once per iteration of the for loop. It is created once, and incremented. In addition, if I understand correctly, method definitions are not closures like other blocks. They retain variable visibility, but do not close over the current value of the variables.

Myrddin Emrys
An in-depth look at the same problem (and solution) in C#: http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx
kejadlen