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!
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
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.
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