Closures and for loops in Ruby

大憨熊 提交于 2020-01-11 05:36:46

问题


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 declared 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

回答1:


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 expression

Syntax

  • 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:

  1. Evaluate the expression. Let O be the resulting value.
  2. 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 is O,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.

  3. 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 Eb.

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.




回答2:


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.



来源:https://stackoverflow.com/questions/2831470/closures-and-for-loops-in-ruby

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!