Introduction
Ruby offers a rich set of iteration methods that go far beyond traditional for loops. Most Ruby developers prefer iterator methods over while or for loops because they’re more expressive, safer (no off-by-one errors), and integrate naturally with blocks.
Fixed-Count Iteration
times
Integer#times iterates a fixed number of times, yielding the current index (0-based):
4.times { |i| puts i }
# => 0
# => 1
# => 2
# => 3
4.times.to_a
# => [0, 1, 2, 3]
# Without the index
3.times { puts "Hello" }
upto
Integer#upto iterates from the receiver up to the argument (inclusive):
1.upto(4) { |i| puts i }
# => 1
# => 2
# => 3
# => 4
1.upto(4).to_a
# => [1, 2, 3, 4]
downto
Integer#downto iterates in reverse:
5.downto(1) { |i| print "#{i} " }
# => 5 4 3 2 1
step
Numeric#step iterates with a custom step size:
1.step(10, 2) { |i| print "#{i} " }
# => 1 3 5 7 9
0.0.step(1.0, 0.25) { |f| print "#{f} " }
# => 0.0 0.25 0.5 0.75 1.0
Range Iteration
Inclusive range (..)
(1..4).to_a
# => [1, 2, 3, 4]
(1..5).each { |i| puts i }
('a'..'e').to_a
# => ["a", "b", "c", "d", "e"]
Exclusive range (…)
(1...4).to_a
# => [1, 2, 3] (excludes 4)
(0...array.length).each { |i| puts array[i] }
Array Iteration
each
The most common iterator โ yields each element:
fruits = ["apple", "banana", "cherry"]
fruits.each { |fruit| puts fruit }
# With index
fruits.each_with_index do |fruit, i|
puts "#{i}: #{fruit}"
end
# => 0: apple
# => 1: banana
# => 2: cherry
# each_with_object โ accumulate into an object
result = fruits.each_with_object({}) do |fruit, hash|
hash[fruit] = fruit.length
end
# => {"apple"=>5, "banana"=>6, "cherry"=>6}
map / collect
Transforms each element, returns a new array:
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]
names = ["alice", "bob", "charlie"]
names.map(&:capitalize)
# => ["Alice", "Bob", "Charlie"]
select / filter
Returns elements where the block is truthy:
numbers = [1, 2, 3, 4, 5, 6]
evens = numbers.select { |n| n.even? }
# => [2, 4, 6]
odds = numbers.reject { |n| n.even? }
# => [1, 3, 5]
reduce / inject
Accumulates a result across all elements:
numbers = [1, 2, 3, 4, 5]
sum = numbers.reduce(0) { |acc, n| acc + n }
# => 15
product = numbers.reduce(:*)
# => 120
# Build a hash from an array
words = ["hello", "world", "ruby"]
lengths = words.reduce({}) { |h, w| h.merge(w => w.length) }
# => {"hello"=>5, "world"=>5, "ruby"=>4}
each_slice and each_cons
(1..10).each_slice(3) { |s| p s }
# => [1, 2, 3]
# => [4, 5, 6]
# => [7, 8, 9]
# => [10]
(1..5).each_cons(3) { |c| p c }
# => [1, 2, 3]
# => [2, 3, 4]
# => [3, 4, 5]
Hash Iteration
scores = { alice: 95, bob: 87, charlie: 92 }
scores.each { |name, score| puts "#{name}: #{score}" }
# Map to a new hash
doubled = scores.transform_values { |v| v * 2 }
# => {alice: 190, bob: 174, charlie: 184}
# Filter
passing = scores.select { |_, score| score >= 90 }
# => {alice: 95, charlie: 92}
Conditional Loops
while
Runs while the condition is true:
i = 0
while i < 5
puts i
i += 1
end
# Inline form
i = 0
i += 1 while i < 5
puts i # => 5
until
Runs until the condition becomes true (opposite of while):
i = 0
until i >= 5
puts i
i += 1
end
# Inline form
i = 0
i += 1 until i >= 5
loop
Infinite loop โ use break to exit:
i = 0
loop do
break if i >= 5
puts i
i += 1
end
# Common pattern: loop with break on condition
result = loop do
input = gets.chomp
break input if input =~ /\A\d+\z/
puts "Please enter a number"
end
for…in
Ruby has for...in but it’s rarely used โ each is preferred:
# Rarely used
for i in 1..5
puts i
end
# Preferred
(1..5).each { |i| puts i }
The key difference: for doesn’t create a new scope for the block variable, while each does.
Controlling Loop Flow
next (like continue)
Skip to the next iteration:
(1..10).each do |i|
next if i.even?
puts i
end
# => 1 3 5 7 9
break
Exit the loop early, optionally returning a value:
result = [1, 2, 3, 4, 5].each do |i|
break "found #{i}" if i == 3
end
puts result # => "found 3"
redo
Restart the current iteration (use carefully):
attempts = 0
[1, 2, 3].each do |i|
attempts += 1
redo if attempts < 2 && i == 1 # retry first element once
puts "#{i} (attempt #{attempts})"
end
Lazy Iteration
For large or infinite sequences, use lazy to avoid computing all values upfront:
# Without lazy: generates all 1M numbers first
(1..1_000_000).select { |n| n.odd? }.first(5)
# With lazy: stops as soon as 5 are found
(1..Float::INFINITY).lazy.select { |n| n.odd? }.first(5)
# => [1, 3, 5, 7, 9]
# Infinite Fibonacci sequence
fibs = Enumerator.new do |y|
a, b = 0, 1
loop { y << a; a, b = b, a + b }
end
fibs.lazy.select { |n| n.even? }.first(5)
# => [0, 2, 8, 34, 144]
Choosing the Right Iterator
| Use case | Method |
|---|---|
| Fixed count | n.times |
| Range of numbers | (a..b).each or a.upto(b) |
| Transform array | map |
| Filter array | select / reject |
| Accumulate value | reduce / inject |
| Iterate with index | each_with_index |
| Accumulate into object | each_with_object |
| Infinite loop | loop |
| Conditional loop | while / until |
| Large/infinite sequences | lazy |
Comments