for loops in Ruby
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