In Progress
Unit 1, Lesson 1
In Progress

For

Video transcript & code

When you're new to Ruby, having come from some other language like Java or Perl, the heavy reliance on methods taking blocks can seem alien and confusing at first. As a result, you might be tempted to rely on more familiar constructs. One such familiar feature you may have used is Ruby's for loop.

At first glance, the for loop is similar to foreach loops found in other languages. We write for, followed by the name of a loop variable, followed by the word in, and then a list of things we want to iterate over. Inside the loop, we perform some operation. Then we end the loop with an end.

for grocery in %W[milk bread cheese]
  puts "Don't forget to buy #{grocery}"
end
# >> Don't forget to buy milk
# >> Don't forget to buy bread
# >> Don't forget to buy cheese

We can also loop over a range of numbers if we want.

for n in 1..3
  puts n
end
# >> 1
# >> 2
# >> 3

for loops look so basic and so similar to the loops found in other languages that we might be tempted to think of them as "simpler" or more "low level" constructs than sending .each to an object. But this isn't really the case at all. In fact, a for loop is simply syntax sugar for calling .each. The loop we started with is equivalent to an idiomatic Ruby .each loop:

%W[milk bread cheese].each do |grocery|
  puts "Don't forget to buy #{grocery}"
end
# >> Don't forget to buy milk
# >> Don't forget to buy bread
# >> Don't forget to buy cheese

What do I mean by "equivalent"? To find out just how equivalent these two forms are, lets write a new class with just one method: #each. Then let's instantiate an object.

First, we'll try explicitly sending it the #each message.

Then we'll change it to a for loop.

class GroceryList
  def each
    yield "yams"
    yield "leeks"
    yield "rutabagas"
  end
end

list = GroceryList.new
for grocery in list
  puts "Don't forget to buy #{grocery}"
end
# >> Don't forget to buy yams
# >> Don't forget to buy leeks
# >> Don't forget to buy rutabagas

As we can see, the for loop is simply using the #each method we defined.

And that's exactly what was going on before as well. When we used a for loop on an array, Ruby used that array's #each method. Ruby Range objects also have an #each method, which is what makes it possible to use them in a for loop.

(1..3).each do |n|
  puts n
end
# >> 1
# >> 2
# >> 3

Apart from the difference in syntax, there is exactly one way in which a for loop differs from .each. Unlike a block, the for loop does not introduce a new block scope for variables.

Let me demonstrate what I mean. If we try to take a look at the value of n after the loop, we get an error. The block argument n was only valid within the body of the loop.

(1..3).each do |n|
  puts n
end
puts "Ending value of n: #{n}"
# ~> -:4:in `<main>': undefined local variable or method `n' for main:Object (NameError)
# >> 1
# >> 2
# >> 3

But if we change this code to a for loop, we can see that at the end of execution, the final value of n hangs around.

for n in (1..3)
  puts n
end
puts "Ending value of n: #{n}"
# >> 1
# >> 2
# >> 3
# >> Ending value of n: 3

This could conceivably get us into trouble someday. For instance, here's a contrived method which counts down from a number n, using a for loop. At the end, it's supposed to reiterate what number it counted down from. But when we execute it with the number 4, it claims that it counted down from 1.

def countdown_from(n)
  for n in n.downto(1)
    puts n
  end
  puts "I counted down from #{n}"
end

countdown_from(4)
# >> 4
# >> 3
# >> 2
# >> 1
# >> I counted down from 1

This is because we foolishly re-used the variable name n from the method's parameter list as the loop counter as well. Since the for loop executes in the same variable scope as the surrounding code, the original argument is overwritten.

Compare this to what happens when we rewrite the code to use an #each loop.

def countdown_from(n)
  n.downto(1).each do |n|
    puts n
  end
  puts "I counted down from #{n}"
end

countdown_from(4)
# >> 4
# >> 3
# >> 2
# >> 1
# >> I counted down from 4

This time, we get the output we expected. This happens because the .each block introduces a new variable scope. The n variable inside this scope "shadows" the outer n, and when the block ends, the inner n variable goes away as well.

So now that we understand what a for loop is and how it differs from #each, what exactly is for good for?

I've heard it claimed that a for loop somehow "looks" better in mixed Ruby/HTML templating languages such as ERB. But I don't find this argument very persuasive.

And I'll be honest: in over a decade of writing Ruby code, I have not once found a reason to prefer a for loop's version of variable scoping. It offers no advantages, and slightly increases the chances of coding errors.

Add to that the fact that Ruby essentially compiles it into a call to #each, but the for syntax obscures what's really going on.

The upshot of all this is that in my opinion, the best thing to with Ruby's for loops is nothing at all. Pretend they don't exist. If you use them anywhere in your code, get out of the habit. If you don't use them, great; there's no reason to start.

.each loops with blocks might be confusing when we're first getting into the language, but the concept of telling collections to iterate over themselves using .each messages is fundamental to the Ruby way. The sooner we internalize that model, the better for us as Ruby programmers.

Happy hacking!

Responses