Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby for loop a trap?

Tags:

ruby

In a discussion of Ruby loops, Niklas B. recently talked about for loop 'not introducing a new scope', as compared to each loop. I'd like to see some examples of how does one feel this.

O.K., I expand the question: Where else in Ruby do we see what apears do/end block delimiters, but there is actually no scope inside? Anything else apart from for ... do ... end?

O.K., One more expansion of the question, is there a way to write for loop with curly braces { block } ?

like image 801
Boris Stitnicky Avatar asked May 01 '12 10:05

Boris Stitnicky


3 Answers

Let's illustrate the point by an example:

results = []
(1..3).each do |i|
  results << lambda { i }
end
p results.map(&:call)  # => [1,2,3]

Cool, this is what was expected. Now check the following:

results = []
for i in 1..3
  results << lambda { i }
end
p results.map(&:call)  # => [3,3,3]

Huh, what's going on? Believe me, these kinds of bugs are nasty to track down. Python or JS developers will know what I mean :)

That alone is a reason for me to avoid these loops like the plague, although there are more good arguments in favor of this position. As Ben pointed out correctly, using the proper method from Enumerable almost always leads to better code than using plain old, imperative for loops or the fancier Enumerable#each. For instance, the above example could also be concisely written as

lambdas = 1.upto(3).map { |i| lambda { i } }
p lambdas.map(&:call)

I expand the question: Where else in Ruby do we see what apears do/end block delimiters, but there is actually no scope inside? Anything else apart from for ... do ... end?

Every single one of the looping constructs can be used that way:

while true do
  #...
end

until false do
  # ...
end

On the other hand, we can write every one of these without the do (which is obviously preferrable):

for i in 1..3
end

while true
end

until false
end

One more expansion of the question, is there a way to write for loop with curly braces { block }

No, there is not. Also note that the term "block" has a special meaning in Ruby.

like image 133
Niklas B. Avatar answered Nov 16 '22 00:11

Niklas B.


First, I'll explain why you wouldn't want to use for, and then explain why you might.

The main reason you wouldn't want to use for is that it's un-idiomatic. If you use each, you can easily replace that each with a map or a find or an each_with_index without a major change of your code. But there's no for_map or for_find or for_with_index.

Another reason is that if you create a variable within a block within each, and it hasn't been created before-hand, it'll only stay in existance for as long as that loop exists. Getting rid of variables once you have no use for them is a good thing.

Now I'll mention why you might want to use for. each creates a closure for each loop, and if you repeat that loop too many times, that loop can cause performance problems. In https://stackoverflow.com/a/10325493/38765 , I posted that using a while loop rather than a block made it slower.

RUN_COUNT = 10_000_000
FIRST_STRING = "Woooooha"
SECOND_STRING = "Woooooha"

def times_double_equal_sign
  RUN_COUNT.times do |i|
    FIRST_STRING == SECOND_STRING
  end
end

def loop_double_equal_sign
  i = 0
  while i < RUN_COUNT
    FIRST_STRING == SECOND_STRING
    i += 1
  end
end

times_double_equal_sign consistently took 2.4 seconds, while loop_double_equal_sign was consistently 0.2 to 0.3 seconds faster.

In https://stackoverflow.com/a/6475413/38765 , I found that executing an empty loop took 1.9 seconds, whereas executing an empty block took 5.7 seconds.

Know why you wouldn't want to use for, know why you would want to use for, and only use the latter when you need to. Unless you feel nostalgic for other languages. :)

like image 20
Andrew Grimm Avatar answered Nov 16 '22 02:11

Andrew Grimm


Well, even blocks are not perfect in Ruby prior to 1.9. They don't always introduce new scope:

i = 0
results = []
(1..3).each do |i|
  results << lambda { i }
end
i = 5
p results.map(&:call)  # => [5,5,5]
like image 40
Victor Moroz Avatar answered Nov 16 '22 02:11

Victor Moroz