Loops
Ruby admits several iteration forms, but the conventional discipline favours iterator methods (each, map, select, reduce, etc.) over explicit loops. The explicit forms are while (loop while condition is true), until (loop while condition is false), loop (infinite — exit with break), and for/in (rarely used). The conventional Ruby idiom is (1..10).each { |n| ... } rather than for n in 1..10. The combination — iterator methods as the conventional default, explicit forms for unusual cases, blocks as the principal iteration mechanism, the substantial Enumerable methods — is the substance of Ruby’s iteration surface.
while and until
while condition
body
end
until condition # equivalent to while !condition
body
end
Examples:
i = 0
while i < 10
puts i
i += 1
end
n = 100
until n <= 0
n -= 5
end
The modifier (postfix) forms:
i += 1 while i < 10
n -= 5 until n <= 0
The conventional discipline:
- Use
while/untilwhen the condition is the natural termination signal. - Use the modifier form for short single-statement loops.
- Prefer iterator methods for substantial iteration.
loop
The loop method admits an infinite loop:
loop do
input = gets
break if input.chomp == "quit"
puts "got: #{input}"
end
The conventional uses are read-eval-print loops, retry loops, and event loops — any loop where termination is internal (via break).
loop is a method — not a keyword. It accepts a block. The mechanism admits substantial flexibility:
result = loop do
attempt = try_once
break attempt if attempt.success?
end
The break value admits returning a value from the loop.
for/in
The for/in admits the C-family iteration form:
for x in [1, 2, 3]
puts x
end
for i in 1..10
puts i
end
The form is rare in idiomatic Ruby. The conventional alternative is each:
[1, 2, 3].each { |x| puts x }
(1..10).each { |i| puts i }
The principal differences:
fordoes not introduce a new scope; the loop variable persists after the loop.eachintroduces a block scope; the variable is local.
for x in [1, 2, 3]
# ...
end
puts x # 3 (still in scope)
[1, 2, 3].each { |y| ... }
# puts y # NameError
The block-scoping is the conventional reason to prefer each.
Iterator methods
The conventional Ruby iteration uses methods on collections:
[1, 2, 3].each { |n| puts n } # iterate
[1, 2, 3].map { |n| n * 2 } # transform → [2, 4, 6]
[1, 2, 3].select { |n| n.odd? } # filter → [1, 3]
[1, 2, 3].reject { |n| n.odd? } # filter out → [2]
[1, 2, 3].reduce(0) { |sum, n| sum + n } # aggregate → 6
[1, 2, 3].count { |n| n > 1 } # count → 2
[1, 2, 3].any? { |n| n > 2 } # → true
[1, 2, 3].all? { |n| n > 0 } # → true
[1, 2, 3].find { |n| n > 1 } # first match → 2
Treated in Enumerable.
times, upto, downto, step
Integer methods admit C-style index loops:
5.times do |i|
puts i # 0, 1, 2, 3, 4
end
1.upto(5) { |i| puts i } # 1, 2, 3, 4, 5
5.downto(1) { |i| puts i } # 5, 4, 3, 2, 1
0.step(20, 5) { |i| puts i } # 0, 5, 10, 15, 20
(1..10).step(2) { |i| puts i } # 1, 3, 5, 7, 9
The forms are conventional for index-driven iteration; for collections, each_with_index is the conventional alternative.
each_with_index
["a", "b", "c"].each_with_index do |item, i|
puts "#{i}: #{item}"
end
# 0: a
# 1: b
# 2: c
# Or with index first:
["a", "b", "c"].each.with_index(1) do |item, i| # custom start
puts "#{i}. #{item}"
end
# 1. a
# 2. b
# 3. c
break, next, redo
[1, 2, 3, 4, 5].each do |n|
break if n > 3 # exit the loop
next if n.even? # skip to next iteration
puts n # 1, 3
end
The break exits the innermost loop; next proceeds to the next iteration. The redo re-executes the current iteration without changing the index — rarely used.
break value returns the given value from the loop:
result = [1, 2, 3].each do |n|
break "found #{n}" if n == 2
end
puts result # "found 2"
break works in iterator methods too:
([1, 2, 3].map { |n| break "stopped" if n > 2; n * 2 })
# "stopped"
return from a block
return inside a block returns from the enclosing method — not from the block:
def find_positive(arr)
arr.each do |n|
return n if n > 0 # returns from find_positive
end
nil
end
find_positive([-1, -2, 3, -4]) # 3
The mechanism admits substantial control flow; for “return from block only”, next returns from the block:
results = arr.map do |x|
next 0 if x.nil? # returns 0 from this block iteration
x * 2
end
Range iteration
(1..10).each { |n| puts n } # 1 to 10
(1...10).each { |n| puts n } # 1 to 9
(1..).take(5).each { |n| puts n } # endless range; take 5
# Step:
(1..20).step(3) { |n| puts n } # 1, 4, 7, 10, 13, 16, 19
# Reverse:
10.downto(1) { |n| puts n } # 10, 9, ..., 1
(1..10).to_a.reverse.each { |n| puts n }
(1..10).reverse_each { |n| puts n }
each_slice and each_cons
[1, 2, 3, 4, 5, 6].each_slice(2) { |s| puts s.inspect }
# [1, 2]
# [3, 4]
# [5, 6]
[1, 2, 3, 4, 5].each_cons(3) { |s| puts s.inspect }
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]
The forms admit substantial windowing patterns.
Common patterns
Accumulating a sum
# Imperative:
sum = 0
[1, 2, 3, 4, 5].each { |n| sum += n }
# Functional:
sum = [1, 2, 3, 4, 5].sum # 15
# Or with reduce:
sum = [1, 2, 3, 4, 5].reduce(0, :+)
sum = [1, 2, 3, 4, 5].reduce(0) { |a, b| a + b }
Transforming a collection
# Imperative:
result = []
items.each { |x| result << x.upcase }
# Functional:
result = items.map(&:upcase)
Filtering
# Imperative:
result = []
items.each { |x| result << x if x.length > 5 }
# Functional:
result = items.select { |x| x.length > 5 }
Iterating with index
items.each_with_index do |item, i|
puts "[#{i}] #{item}"
end
# Or with map:
items.map.with_index { |item, i| "[#{i}] #{item}" }
Range over numbers
total = (1..100).sum # 5050
squares = (1..10).map { |n| n * n }
even_count = (1..100).count(&:even?) # 50
Loop with explicit termination
loop do
job = queue.dequeue
break if job.nil?
process(job)
end
Polling
until ready?
sleep 0.1
end
Counter pattern
counts = Hash.new(0)
words.each { |w| counts[w] += 1 }
The Hash.new(0) admits substantial conciseness — missing keys default to 0.
Searching with find
result = items.find { |x| x.matches? }
if result
process(result)
end
# Or return the result of an iterator method:
items.find { |x| x.id == target }
Eager vs lazy iteration
# Eager (constructs the entire intermediate array):
(1..Float::INFINITY).map { |n| n * n }.first(5) # never returns
# Lazy:
(1..Float::INFINITY).lazy.map { |n| n * n }.first(5)
# [1, 4, 9, 16, 25]
The lazy admits substantial efficiency for substantial or infinite collections.
zip for parallel iteration
names = ["alice", "bob", "charlie"]
ages = [30, 25, 35]
names.zip(ages).each do |name, age|
puts "#{name}: #{age}"
end
Chained iteration
result = items
.reject(&:archived?)
.map(&:title)
.sort
.first(10)
The pattern admits substantial functional-style processing.
Iterating with state
total = 0
running_totals = items.map do |x|
total += x
total
end
# Or:
running_totals = items.each_with_object([0]) do |x, totals|
totals << totals.last + x
end[1..]
The each_with_object admits “iterate while mutating an accumulator”.
inject / reduce for aggregation
# Sum:
sum = [1, 2, 3].inject(0) { |a, b| a + b }
sum = [1, 2, 3].inject(:+) # symbol shorthand
sum = [1, 2, 3].sum # built-in
# Maximum:
max = [3, 1, 4, 1, 5, 9].inject { |a, b| a > b ? a : b }
max = [3, 1, 4, 1, 5, 9].max
# Build a hash:
counts = words.inject(Hash.new(0)) { |h, w| h[w] += 1; h }
# Or with each_with_object:
counts = words.each_with_object(Hash.new(0)) { |w, h| h[w] += 1 }
Generators with Enumerator
fibonacci = Enumerator.new do |y|
a, b = 0, 1
loop do
y << a
a, b = b, a + b
end
end
fibonacci.first(10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
The form admits substantial lazy generators.
loop with retry
attempts = 0
loop do
attempts += 1
result = try_operation
break result if result.success?
break nil if attempts >= 3
sleep 1
end
A note on iteration vs explicit loops
The conventional Ruby discipline strongly favours iteration methods over explicit loops:
# Conventional (iteration method):
items.each { |x| process(x) }
items.map { |x| transform(x) }
items.select { |x| keep?(x) }
# Less conventional (explicit loop):
i = 0
while i < items.length
process(items[i])
i += 1
end
# Even less conventional:
for item in items
process(item)
end
The iteration-method form admits:
- Block-scoped iteration variables — no leak.
- Substantial composition — chain
select.map.first(5). - Enumerable mixin — many methods derived from
each. - Lazy evaluation via
lazy. - Better integration with the standard library.
A note on the conventional discipline
The contemporary Ruby loops advice:
- Use
eachfor plain iteration. - Use
map,select,reject,reducefor transformations. - Use
times,upto,downto,stepfor index-driven loops. - Use ranges for numeric iteration.
- Use
loopfor explicit infinite loops with internal termination. - Use
while/untilfor condition-driven loops. - Use modifier forms (postfix
while/until) for short loops. - Use
each_with_indexfor index access. - Use
each_with_objectfor accumulator patterns. - Use
lazyfor substantial or infinite sequences. - Avoid
for/in— useeachinstead. - Use
break valueto return from a loop. - Use
returnto return from the enclosing method (not just the block).
The combination — while/until/loop for explicit loops, times/upto/step/each for iteration, the substantial Enumerable methods derived from each, the rare-but-admitted for/in form — is the substance of Ruby’s iteration surface. The discipline produces concise, expressive iteration code that admits substantial composition.