Closures and for loops in Ruby
I'm kind of new to Ruby and some of the closure logic has me a confused. Consider this code:
array = []
for i in (1..5)
array << lambda {i}
end
array.map{|f| f.call} # => [5, 5, 5, 5, 5]
This makes sense to me because i is bound outside the loop, so the same variable is captured by each trip through the loop. It also makes sense to me that using an each block can fix this:
array = []
(1..5).each{|i| array << lambda {i}}
array.map{|f| f.call} # => [1, 2, 3, 4, 5]
...because i is now being declare开发者_如何学运维d separately for each time through. But now I get lost: why can't I also fix it by introducing an intermediate variable?
array = []
for i in 1..5
j = i
array << lambda {j}
end
array.map{|f| f.call} # => [5, 5, 5, 5, 5]
Because j is new each time through the loop, I'd think a different variable would be captured on each pass. For example, this is definitely how C# works, and how -- I think-- Lisp behaves with a let. But in Ruby not so much. What's really happening?
Edit: See comments in the answers; the problem seems to be that j is still in scope outside the loop. How does scope in loops really work?
Edit: I guess I still don't understand; if loops don't create new scopes, why this:
for i in 1..5
puts j if i > 1 #undefined local variable or method `j' for main:Object (NameError)
j = i
end
Okay, this is getting ridiculous. Every time I try to answer a question about how for
loops work in Ruby, I get it wrong.
The reason for this is, of course, that I don't use for
loops in Ruby, neither does anybody else, so it really doesn't matter to me :-)
Anyway, to settle the matter once and for all, I went straight to the ultimate source, the December 1, 2009 preliminary Draft of the IPA Ruby Language Specification (destined to become the ISO Ruby Language Specification):
§11.4.1.2.3 The
for
expressionSyntax
- for-expression → for for-variable in expression do-clause end
- for-variable → left-hand-side | multiple-left-hand-side
The expression of a for-expression shall not be a jump-expression.
Semantics
A for-expression is evaluated as follows:
- Evaluate the expression. Let
O
be the resulting value.Let
E
be the primary-method-invocation of the form primary-expression [no line-terminator here].each do | block-formal-argument-list | block-body end, where the value of the primary-expression isO
,the block-formal-argument-list is the for-variable, the block-body is the compound-statement of the do-clause.Evaluate
E
, but skip Step c of §11.2.2.The value of the for-expression is the resulting value of the invocation.
Okay, so basically this means that
for for_variable in expression
do_clause
end
gets translated to
O = expression
O.each do |for_variable|
do_clause
end
Or, in your case:
for i in 1..5
puts j if i > 1 #undefined local variable or method `j' (NameError)
j = i
end
gets translated to
(1..5).each do |i|
puts j if i > 1 #no excpetion here, works just fine ??!!??
j = i
end
Aha! But we forgot something! There's this ominous "skip Step c of §11.2.2." thing! So, what does it say?
- Push an empty set of local variable bindings onto ⟦local-variable-bindings⟧.
Note that Step b
- Set the execution context to
E
b
.
is not skipped.
So, as far as I can see, a for
loop gets its own execution context, which starts out as a copy of the current execution context, but it does not get its own set of local variable bindings. IOW: it gets its own dynamic execution context, but not its own lexical scope.
I must admit, I'm still not sure I fully understand it, but it doesn't get any more precise than this.
What version of Ruby are you running this on? 1.8 does not have block scope for local variables, so j is still hanging around (and equal to 5) even after the end of the for
.
精彩评论