Loops #
Loops are one of the most fundamental constructs in programming — but the way Ruby expresses them is very different from languages like C, Java, or JavaScript. In Ruby, condition-based loops (while, until) exist, but they’re not the most commonly used. What dominates instead is block-based iteration from the Enumerable module — each, map, select, reduce, and dozens of other methods already built into Array, Hash, Range, and every collection. Understanding the difference between “loops” and “iterators” in Ruby, and when to use each, is the key to writing truly idiomatic code.
Condition-Based Loops #
Condition-based loops run a code block while a certain condition holds. Ruby provides while, until, and loop for this purpose.
while #
while runs the code block while its condition is truthy:
count = 1
while count <= 5
puts "Iteration #{count}"
count += 1
end
# => Iteration 1, 2, ... 5
while is most appropriate when the number of iterations isn’t known in advance and depends on an external condition that changes:
# Real-world example: polling until a condition is met
def wait_until_done(task, max_seconds: 30)
elapsed = 0
while !task.done? && elapsed < max_seconds
sleep 1
elapsed += 1
end
task.done? ? "Done in #{elapsed} seconds" : "Timeout after #{max_seconds} seconds"
end
until #
until is the opposite of while — it runs the code block while its condition is falsy. It reads more naturally for negatively phrased conditions:
queue = ["task A", "task B", "task C"]
until queue.empty?
task = queue.shift
puts "Processing: #{task}"
end
# => Processing: task A, task B, task C
while and until also support a postfix modifier form — useful for simple one-line conditions:
# Postfix modifier while
sleep 0.5 while server_not_ready?
# Postfix modifier until
resend until success? || attempts >= 3
loop — Infinite Loops #
loop runs the code block forever until explicitly stopped with break. This is the idiomatic Ruby way to write an infinite loop:
# ANTI-PATTERN: while true — C style, not Ruby
while true
input = gets.chomp
break if input == "exit"
puts "You typed: #{input}"
end
# CORRECT: use loop
loop do
input = gets.chomp
break if input == "exit"
puts "You typed: #{input}"
end
loop is very useful for implementing a REPL (Read-Eval-Print Loop) or a simple event loop:
def run_repl
loop do
print "ruby> "
input = gets&.chomp
break if input.nil? || input == "exit"
begin
result = eval(input)
puts "=> #{result.inspect}"
rescue => e
puts "Error: #{e.message}"
end
end
end
for — The Rarely Used Loop #
for exists in Ruby, but it’s almost never used by experienced Ruby developers. There’s a technical reason behind it:
# for loop — valid but not idiomatic
for n in 1..5
puts n
end
# PROBLEM: variables defined inside a for loop
# leak into the outer scope!
for item in ["a", "b", "c"]
result = item.upcase
end
puts result # => "C" (still accessible outside the loop!)
# each doesn't have this problem
["a", "b", "c"].each do |item|
inner_result = item.upcase
end
puts inner_result # => NameError: variable doesn't leak out of the block
Why for is rarely used in Ruby:
✗ Local variables inside for leak into the outer scope
✗ Doesn't support method chaining
✗ Can't be used with other Enumerables (Hash, Range with step, etc.)
✓ each solves all these problems and is more expressive
Enumerable Iterators — The Idiomatic Ruby Way #
Enumerable is a module that provides dozens of iteration methods for every collection in Ruby — Array, Hash, Range, and any class that implements each. This is the most idiomatic way to loop in Ruby.
flowchart TD
A[Enumerable] --> B[Basic Iteration\neach, each_with_index\neach_with_object]
A --> C[Transformation\nmap/collect\nflat_map\nzip]
A --> D[Filtering\nselect/filter\nreject\nfind/detect]
A --> E[Accumulation\nreduce/inject\nsum, count\nmin, max, minmax]
A --> F[Grouping\ngroup_by\nchunk, tally\npartition]
A --> G[Checks\nany?, all?\nnone?, one?]each — Basic Iteration #
each is the most fundamental iterator — it runs a block for every element without changing or collecting the results:
fruits = ["apple", "mango", "orange", "pineapple"]
# do...end block syntax — for multi-line
fruits.each do |f|
puts f.upcase
end
# { } syntax — for one-liners
fruits.each { |f| puts f.upcase }
# Hash — the block receives key and value
profile = { name: "Rina", age: 28, city: "Bandung" }
profile.each do |key, value|
puts "#{key}: #{value}"
end
# Range
(1..5).each { |n| print "#{n} " } # => 1 2 3 4 5
each_with_index and each_with_object #
When you need an index during iteration, use each_with_index. When you need to accumulate results into a single object, use each_with_object:
list = ["laptop", "mouse", "keyboard", "monitor"]
# each_with_index — index starts at 0
list.each_with_index do |item, i|
puts "#{i + 1}. #{item}"
end
# => 1. laptop, 2. mouse, 3. keyboard, 4. monitor
# ANTI-PATTERN: building a Hash manually with each
result = {}
list.each_with_index { |item, i| result[item] = i }
# CORRECT: each_with_object is more concise
result = list.each_with_object({}) do |item, hash|
hash[item] = item.length
end
puts result.inspect
# => {"laptop"=>6, "mouse"=>5, "keyboard"=>8, "monitor"=>7}
times, upto, downto, step #
For simple number-based loops, Ruby provides methods directly on Integer:
# times — repeat N times, index starts at 0
5.times { |i| print "#{i} " } # => 0 1 2 3 4
# upto — count up from a to b
1.upto(5) { |n| print "#{n} " } # => 1 2 3 4 5
# downto — count down from a to b
5.downto(1) { |n| print "#{n} " } # => 5 4 3 2 1
# step — with a custom increment
1.step(10, 2) { |n| print "#{n} " } # => 1 3 5 7 9
10.step(1, -3) { |n| print "#{n} " } # => 10 7 4 1
# Range also has step
(0.0..1.0).step(0.25) { |n| print "#{n} " }
# => 0.0 0.25 0.5 0.75 1.0
Transforming Collections #
map / collect — Change Every Element #
map returns a new Array with the result of applying the block to every element. It’s a transformation operator — it doesn’t change the original array:
numbers = [1, 2, 3, 4, 5]
# Square every element
squares = numbers.map { |n| n ** 2 }
puts squares.inspect # => [1, 4, 9, 16, 25]
# String transformations
names = ["rina", "budi", "citra"]
puts names.map(&:capitalize).inspect # => ["Rina", "Budi", "Citra"]
puts names.map(&:upcase).inspect # => ["RINA", "BUDI", "CITRA"]
# Transforming complex objects
products = [
{ name: "Laptop", price: 15_000_000 },
{ name: "Mouse", price: 350_000 },
{ name: "Monitor", price: 3_500_000 }
]
# Take only the names
products.map { |p| p[:name] }
# => ["Laptop", "Mouse", "Monitor"]
# Apply a 10% discount
products.map { |p| p.merge(price: (p[:price] * 0.9).round) }
map! is the destructive version that modifies the original array. Use it with care:
# ANTI-PATTERN: map! changes the original array — dangerous if unintentional
data = [1, 2, 3]
data.map! { |n| n * 2 }
puts data.inspect # => [2, 4, 6] (original array changed!)
# CORRECT: use map (non-destructive) and assign the result
data = [1, 2, 3]
result = data.map { |n| n * 2 }
puts data.inspect # => [1, 2, 3] (unchanged)
puts result.inspect # => [2, 4, 6]
flat_map — Map and Flatten at Once #
flat_map applies a block to every element, then flattens the result one level:
sentences = ["Ruby is fun", "Python is nice too"]
# Regular map produces an array of arrays
sentences.map { |s| s.split(" ") }
# => [["Ruby", "is", "fun"], ["Python", "is", "nice", "too"]]
# flat_map flattens directly
sentences.flat_map { |s| s.split(" ") }
# => ["Ruby", "is", "fun", "Python", "is", "nice", "too"]
# Another example: collect all tags from many articles
articles = [
{ title: "Ruby Basics", tags: [:ruby, :beginner] },
{ title: "Rails Guide", tags: [:ruby, :rails, :web] },
{ title: "Python Tips", tags: [:python, :tips] }
]
all_tags = articles.flat_map { |a| a[:tags] }.uniq
puts all_tags.inspect
# => [:ruby, :beginner, :rails, :web, :python, :tips]
Filtering Collections #
select / filter — Pick Those Meeting the Condition #
select returns a new Array containing the elements for which the block evaluates to true:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even = numbers.select { |n| n.even? }
puts even.inspect # => [2, 4, 6, 8, 10]
large = numbers.select { |n| n > 5 }
puts large.inspect # => [6, 7, 8, 9, 10]
# On a Hash — returns a new Hash
config = { host: "localhost", port: 5432, debug: false, verbose: true }
active = config.select { |_, v| v }
puts active.inspect # => {host: "localhost", port: 5432, verbose: true}
reject — The Opposite of select #
reject returns the elements for which the block evaluates to false:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# ANTI-PATTERN: select with negation — harder to read
odd = numbers.select { |n| !n.even? }
# CORRECT: reject is more expressive
odd = numbers.reject { |n| n.even? }
puts odd.inspect # => [1, 3, 5, 7, 9]
# Very useful for nil filtering
data = [1, nil, 2, nil, 3, nil]
clean = data.reject(&:nil?) # same as data.compact
puts clean.inspect # => [1, 2, 3]
find / detect — Find a Single Element #
find returns the first element meeting the condition, or nil if none match:
numbers = [3, 7, 12, 5, 18, 2]
first_even = numbers.find { |n| n.even? }
puts first_even # => 12
# With a default value if not found
none = numbers.find { |n| n > 100 }
puts none.inspect # => nil
# find_index — returns the index, not the value
puts numbers.find_index { |n| n.even? } # => 2
Accumulation and Reduction #
reduce / inject — Accumulate into a Single Value #
reduce (alias inject) combines all elements of a collection into one value through a repeated operation:
numbers = [1, 2, 3, 4, 5]
# Block syntax
sum = numbers.reduce(0) { |acc, n| acc + n }
puts sum # => 15
# Symbol syntax — more concise
sum = numbers.reduce(:+) # => 15
product = numbers.reduce(:*) # => 120
max = numbers.reduce { |a, b| a > b ? a : b } # => 5
# Complex example: building a Hash from an Array
pairs = [[:name, "Budi"], [:age, 30], [:city, "Jakarta"]]
hash = pairs.reduce({}) do |acc, (key, value)|
acc.merge(key => value)
end
puts hash.inspect # => {name: "Budi", age: 30, city: "Jakarta"}
sum, count, min, max, minmax #
For common aggregation operations, Enumerable provides shortcuts that are more expressive than reduce:
numbers = [5, 3, 8, 1, 9, 2, 7]
puts numbers.sum # => 35
puts numbers.count # => 7
puts numbers.count { |n| n > 5 } # => 3 (how many are > 5)
puts numbers.min # => 1
puts numbers.max # => 9
puts numbers.minmax.inspect # => [1, 9]
puts numbers.sum { |n| n * 2 } # => 70 (sum with transformation)
# min_by / max_by — for complex objects
products = [
{ name: "Laptop", price: 15_000_000 },
{ name: "Mouse", price: 350_000 },
{ name: "Monitor", price: 3_500_000 }
]
cheapest = products.min_by { |p| p[:price] }
priciest = products.max_by { |p| p[:price] }
puts cheapest[:name] # => "Mouse"
puts priciest[:name] # => "Laptop"
Grouping #
group_by — Group by a Criterion #
group_by groups elements into a Hash whose keys are the block’s results:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
groups = numbers.group_by { |n| n.even? ? :even : :odd }
puts groups[:even].inspect # => [2, 4, 6, 8, 10]
puts groups[:odd].inspect # => [1, 3, 5, 7, 9]
# Group transactions by month
transactions = [
{ date: "2024-01-15", amount: 100_000 },
{ date: "2024-01-28", amount: 50_000 },
{ date: "2024-02-05", amount: 200_000 },
{ date: "2024-02-20", amount: 75_000 },
]
per_month = transactions.group_by { |t| t[:date][0..6] }
per_month.each do |month, data|
total = data.sum { |t| t[:amount] }
puts "#{month}: Rp #{total}"
end
# => 2024-01: Rp 150000
# => 2024-02: Rp 275000
tally — Count Frequencies #
tally counts how many times each value appears in a collection:
survey_results = [:satisfied, :unsatisfied, :satisfied, :neutral, :satisfied, :unsatisfied, :satisfied]
frequencies = survey_results.tally
puts frequencies.inspect
# => {:satisfied=>4, :unsatisfied=>2, :neutral=>1}
# The most frequently occurring word
text = "ruby is a fun language and ruby is very expressive"
words = text.split.tally.sort_by { |_, v| -v }
words.first(3).each { |w, n| puts "#{w}: #{n}x" }
# => ruby: 2x, is: 2x, a: 1x
partition — Split into Two Groups #
partition returns two arrays: the elements meeting the condition and those that don’t:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even, odd = numbers.partition { |n| n.even? }
puts even.inspect # => [2, 4, 6, 8, 10]
puts odd.inspect # => [1, 3, 5, 7, 9]
# Split active and inactive users
active, inactive = users.partition { |u| u.active? }
Collection Checks #
numbers = [2, 4, 6, 8, 10]
puts numbers.any? { |n| n > 8 } # => true (at least one > 8)
puts numbers.all? { |n| n.even? } # => true (all even)
puts numbers.none? { |n| n > 20 } # => true (none > 20)
puts numbers.one? { |n| n > 8 } # => true (exactly one > 8)
# Without a block — check whether any element is truthy
[nil, false].any? # => false
[nil, false, 1].any? # => true
[1, 2, 3].all? # => true
[1, nil, 3].all? # => false
Control Flow in Loops #
break — Stop the Loop #
break stops the loop entirely. In Enumerable iterators, it can also return a value:
# break from while
i = 0
while i < 10
break if i == 5
puts i
i += 1
end
# => 0 1 2 3 4
# break with a return value from an iterator
result = [1, 2, 3, 4, 5].each do |n|
break "found: #{n}" if n == 3
end
puts result # => "found: 3"
# Practical for search with a return value
number = (1..Float::INFINITY).lazy.each do |n|
break n if n % 17 == 0 && n % 13 == 0
end
puts number # => 221 (the first number divisible by both 17 and 13)
next — Skip This Iteration #
next skips the rest of the block for the current iteration and moves on to the next element:
(1..10).each do |n|
next if n.even? # skip even numbers
puts n
end
# => 1 3 5 7 9
# Practical for skipping invalid conditions without nesting
data = [nil, 1, nil, 2, 3, nil, 4]
data.each do |item|
next if item.nil? # skip nils — avoid nesting
next if item < 2 # skip values < 2
puts item * 10
end
# => 20 30 40
redo — Redo the Current Iteration #
redo restarts the current iteration from the beginning without moving to the next element. It’s rarely used, but useful for retry scenarios inside iteration:
attempts_per_item = Hash.new(0)
["url1", "url2", "url3"].each do |url|
attempts_per_item[url] += 1
begin
# simulate a process that can fail
raise "Connection failed" if attempts_per_item[url] < 3
puts "Successfully processed #{url} on attempt #{attempts_per_item[url]}"
rescue
redo if attempts_per_item[url] < 3 # retry up to 3 times
puts "Failed to process #{url} after 3 attempts"
end
end
Lazy Enumerators — Efficiency for Large Collections #
By default, all Enumerable iterators evaluate the entire collection before returning results. For very large or infinite collections, this is inefficient. lazy defers evaluation until it’s actually needed:
# ANTI-PATTERN: eager evaluation on a huge range
(1..Float::INFINITY).select { |n| n.odd? }.first(5)
# ← this never finishes because it tries to select all infinite numbers!
# CORRECT: lazy evaluation
(1..Float::INFINITY).lazy.select { |n| n.odd? }.first(5)
# => [1, 3, 5, 7, 9] ← only evaluates until 5 elements are found
# Lazy chain
result = (1..Float::INFINITY)
.lazy
.select { |n| n % 3 == 0 } # divisible by 3
.map { |n| n ** 2 } # square it
.reject { |n| n.to_s.include?("9") } # doesn't contain digit 9
.first(5)
puts result.inspect # => [36, 144, 225, 576, 1296]
# Lazy is also useful for large files — read line by line without loading everything
File.foreach("big_data.csv")
.lazy
.select { |line| line.include?("Jakarta") }
.map { |line| line.split(",").first }
.first(10)
Choosing the Right Loop Method #
flowchart TD
A[Need a loop] --> B{Type of operation?}
B --> C["Run side effects\nwithout collecting results"]
B --> D["Transform every element\ninto a new value"]
B --> E["Select a subset of elements"]
B --> F["Combine into a single value"]
B --> G["Group elements"]
B --> H["Repeated condition,\nnot a collection"]
C --> C1["each\neach_with_index"]
D --> D1["map / flat_map"]
E --> E1["select / reject\nfind / find_index"]
F --> F1["reduce / inject\nsum, min, max, count"]
G --> G1["group_by / tally\npartition"]
H --> H1["while / until / loop"]Quick guide to choosing an iteration method:
each → iterate, side effects, no collection result needed
map → transform every element, return a new collection
flat_map → map + flatten one level
select / filter → filter elements meeting the condition
reject → filter elements NOT meeting the condition
find → find the first element meeting the condition
reduce / inject → accumulate into a single value
sum → shortcut for reduce(:+) with optional transformation
group_by → group into a Hash by a criterion
tally → count element occurrence frequencies
partition → split into two groups (matching / not matching)
any? all? none? → condition checks on a collection
each_with_index → each but you need the index
each_with_object → each but accumulate into a specific object
times / upto → simple number-based loops
while / until → condition-based loops (not collections)
loop → infinite loop with explicit break
.lazy → lazy evaluation for large or infinite collections
Summary #
forloops are almost never used — local variables inside them leak into the outer scope, and they have no advantage overeach.eachfor side effects,mapfor transformation — don’t useeachto build a new collection and don’t usemapif you’re discarding the results.map!changes the original array — safer to usemap(non-destructive) and store the result in a new variable.flat_map=map+flatten(1)— use it when the block returns Arrays and you want the results flattened.reduceis the most flexible — but for common cases (sum, min, max, count) use the more expressive shortcuts.group_byandtallyfor data analysis — more concise than building a Hash manually witheach.breakcan return a value from an iterator — useful for searches with an early exit that returns the found result.nextis cleaner than nesting — usenext if conditionat the start of a block to skip invalid elements rather than wrapping logic inif..lazyis mandatory for infinite collections — withoutlazy,selectormapon(1..Float::INFINITY)will never finish.whileandloopfor non-collection conditions — polling, retry, event loops, and conditions depending on changing external state.