Rubyist generally iterate with #each or one of the innumerable methods in Enumerable. But Ruby also offers for loops. How does the for loop compare to #each, and why don’t we tend to use it?

We can use Array#each to iterate through an array and print each element.

[1, 2, 3].each do |i|
  puts i
end

We can use a for loop to do the same thing.

for i in [1, 2, 3]
  puts i
end

The for loop is a few characters shorter, and is possibly easier for a non-Rubyist to understand at first glance. So why do we reject it?

One reason is that Ruby is an object-oriented programming language. Rather than iterate with for and in, keywords defined by the language, we prefer to send messages to objects. That helps keep the caller of a method independent from the implementation of that method; it is the responsibility of the array to figure out how do respond to #each, and if we want to change that behavior we only need to change the Array object.

class Array
  def each
    puts "No Thanks"
  end
end

Is it possible to change the behavior of a for loop at runtime? It turns out it is, but it is not obvious that should be the case. We can’t override Ruby’s keywords, so why would we be able to change the behavior of a for loop?

Let’s compare some Ruby VM instructions (simplified a bit). The instructions for [1, 2, 3].each appear above the dotted line, and the instructions for the do/end block appear below the dotted line. When we call [1, 2, 3].each, Ruby puts the array onto an internal stack (putobject at 0002), then sends the each message to that array (send at 0004; Array#each is implemented in C as a for loop). Inside the block, Ruby puts the self object onto the internal stack (putself at 0004), puts the value of i onto the stack (getlocal at 0005), then sends the puts method to the self object with i as an argument (send at 0007).

code = <<-RUBY
  [1, 2, 3].each do |i|
    puts i
  end
RUBY

puts RubyVM::InstructionSequence.compile(code).disasm

# 0002 putobject       [1, 2, 3]
# 0004 send            <callinfo!mid:each, argc:0>, <callcache>, block in <compiled>
# |------------------------------------------------------------------------
# 0004 putself          
# 0005 getlocal        0
# 0007 send            <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>

Let’s compare to the instructions for a for loop. The instructions above the dotted line are exactly the same! Ruby’s for loops actually defer to the #each method. Indeed any object with an #each method can be used in a for loop, and changing the behavior of the #each method will change the behavior of the for loop.

code = <<-RUBY
  for i in [1, 2, 3]
    puts i
  end
RUBY

puts RubyVM::InstructionSequence.compile(code).disasm

# 0002 putobject        [1, 2, 3]
# 0004 send             <callinfo!mid:each, argc:0>, <callcache>, block in <compiled>
# |------------------------------------------------------------------------
# 0000 getlocal         0
# 0002 setlocal         1
# 0008 putself          
# 0009 getlocal         1
# 0011 send             <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>

The instructions below the dotted line are a little different. This has to do with a quirk in the scoping of for loops. After putting the value of i onto the stack within the current scope (getlocal at 0000 with an argument of 0) Ruby sets the value of i in the outer scope (setlocal at 0002 with an argument of 1). This means that the variable i is available after the loop is complete.

for i in [1, 2, 3]
  puts i
end

puts i
#=> 3

Trying the same thing with Array#each, we get an error.

[1, 2, 3].each do |i|
  puts i
end

puts i
#=> NameError: undefined local variable or method `i'

When I first came across this behavior, I though it must be a bug. But these virtual machine instructions are no accident. So why pollute the outer scope with this leftover variable? For that matter why offer for loops at all?

Check out a for loop in Python. Look familiar? Notice how i remains in scope after the iteration is complete.

for i in [1, 2, 3]:
  print i

print i
#=> 2

I don’t know if Ruby imitated Python, or if they both imitated some other language. But I do know that Ruby is designed for developer happiness, and that includes the happiness of developers coming to Ruby from other languages. A Python developer would have no trouble coming to Ruby. They could write a for loop and it would behave exactly as they expected. We learn to love Ruby’s idiosyncrasies with time, but they are not a barrier to entry.

End

For those of you interested in JavaScript:

Using var in a for loop can cause problems, since the variable gets reassigned in each iteration, and remains in scope after the iteration is complete.

for(var i = 1; i < 4; i++) {
  console.log(i);
}

console.log(i);
//=> 4

With let you get a fresh variable each time, scoped to the block.

for(let i = 1; i < 4; i++) {
  console.log(i);
}

console.log(i);
//=> ReferenceError: i is not defined