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 variablename
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 aProc
object (the block passed to it) into aMethod
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, yourfor ... in
loop is equivalent to the following (in Ruby 1.8 anyway):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 afor
loop expression gets executed exactly like aneach
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: