16

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 } ?

Community
  • 1
  • 1
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
  • Just a side note (I've said something like that in another comment already): The issue with `for` is not only that it differs in a subtle way from other constructs using `do/end`, but also that it can used only for exactly one reason: traversing an enumerable. Often, constructs from functional programming such as `select` or `map` are much better suited for expressing a problem than a plain old loop, as inherited directly from C. – Niklas B. May 01 '12 at 10:38
  • What about the other way round: You give real-world examples of where you think a `for` loop is needed and we give you a better alternative? :) Otherwise I think this question is just a duplicate of http://stackoverflow.com/questions/3294509/for-vs-each-in-ruby – Niklas B. May 01 '12 at 10:40
  • 1
    How about performance? Isn't plain for faster than entering a closure? And what if you _want_ to define local variables in for loop for later use :) ? (Just quick thougts on what might for be good for :) – Boris Stitnicky May 01 '12 at 11:01
  • 2
    Usually you should care about readability a lot more than about performance, else why would you use Ruby in the first place. Defining a variable inside a loop for later use smells like very bad design. – Niklas B. May 01 '12 at 11:07

3 Answers3

14

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.

Niklas B.
  • 92,950
  • 18
  • 194
  • 224
  • 3
    I'll expand on this, too (awesome answer, Niklas B.). Never say never, but I *never* write nor want to see `for` loops in Ruby code. They're not idiomatic at all and they introduce nasty gotchas for unknowing developers. Use something provided by [`Enumerable`](http://ruby-doc.org/core-1.9.3/Enumerable.html) instead. – Ben Kreeger May 01 '12 at 13:43
  • @Ben: Sure, I am exactly of your opinion and I made that clear in the discussion OP is referring to :) – Niklas B. May 01 '12 at 14:04
  • Ah, I see you have! Thanks for driving it home. – Ben Kreeger May 01 '12 at 15:04
  • @NiklasB.: Now, based on this, will you recommend that just like for loop, while and until loops are also not recommendable for serious use? – Boris Stitnicky May 01 '12 at 16:15
  • 1
    @Boris: Yes, often you can avoid using those as well. However, they don't have an obvious alternative, unlike `for`, so you'll need to think a bit to replace those by better constructs or come to the conclusion that they are actually the best solution to a particular problem. – Niklas B. May 01 '12 at 16:19
3

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. :)

Community
  • 1
  • 1
Andrew Grimm
  • 78,473
  • 57
  • 200
  • 338
1

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]
Niklas B.
  • 92,950
  • 18
  • 194
  • 224
Victor Moroz
  • 9,167
  • 1
  • 19
  • 23
  • In Ruby 1.9, this should produce `[1,2,3]`, as expected (also, it might trigger a warning about the shadowed variable). – Niklas B. May 01 '12 at 14:03
  • @NiklasB. Thanks, good to know, it's a really weak point of R1.8, no warnings in R1.9 though by the way. Just curious how it works with legacy code, I remember it was serious point when it was discussed for 1.9 – Victor Moroz May 01 '12 at 14:14
  • I think they had a warning for it in the early versions of 1.9 to make users of legacy code aware of the problem. It seems to be removed in later versions, though. – Niklas B. May 01 '12 at 14:15
  • I added the fact that this only applies to 1.8 to your answer. Hope you don't mind. – Niklas B. May 01 '12 at 14:19
  • Maybe a nitpick, but I really don't like the use of the phrase "don't introduce a new _scope_." They introduce a new _scope_, but the variable is being referenced in the outer scope. Since blocks close over **variables** not **values**, you get whatever the value of `i` is when the lambda is _called_, not when it is _defined_. – Jon Wingfield Aug 18 '12 at 12:39