Overview
Ruby implements iteration through the Enumerable module, which defines methods for traversing, searching, and manipulating collections. The Enumerable module gets mixed into Array, Hash, Range, and other collection classes, providing consistent iteration behavior across different data structures.
The foundation of Ruby's iteration system centers on the each
method, which yields elements one at a time to a block. Other iteration methods build upon this pattern, using each
internally while providing specialized behavior for common operations like transformation, filtering, and reduction.
[1, 2, 3].each { |n| puts n * 2 }
# Output: 2, 4, 6
[1, 2, 3].map { |n| n * 2 }
# => [2, 4, 6]
[1, 2, 3, 4].select { |n| n.even? }
# => [2, 4]
The Enumerator class provides external iteration capabilities, allowing methods to return enumerator objects when called without blocks. These enumerators can be chained, combined with other enumerators, or used with external iteration control structures.
Ruby's iteration methods fall into several categories: transformation methods (map
, collect
), filtering methods (select
, reject
, filter
), reduction methods (reduce
, inject
), and utility methods (each_with_index
, each_with_object
). Hash-specific iteration methods include each_key
, each_value
, and each_pair
.
Basic Usage
The each
method forms the core of Ruby iteration, executing a block for each element in a collection. The method returns the original collection, making it suitable for side effects rather than transformation.
fruits = ['apple', 'banana', 'cherry']
fruits.each do |fruit|
puts "Processing: #{fruit}"
end
# Returns: ['apple', 'banana', 'cherry']
Transformation methods create new collections based on block results. The map
method applies a block to each element and collects the results into a new array.
numbers = [1, 2, 3, 4, 5]
squares = numbers.map { |n| n ** 2 }
# => [1, 4, 9, 16, 25]
# Hash transformation
prices = { apple: 1.50, banana: 0.75, cherry: 2.00 }
discounted = prices.map { |fruit, price| [fruit, price * 0.9] }.to_h
# => {:apple=>1.35, :banana=>0.675, :cherry=>1.8}
Filtering methods select elements based on block conditions. The select
method keeps elements where the block returns truthy values, while reject
keeps elements where the block returns falsy values.
ages = [12, 18, 25, 16, 30, 14]
adults = ages.select { |age| age >= 18 }
# => [18, 25, 30]
minors = ages.reject { |age| age >= 18 }
# => [12, 16, 14]
The reduce
method accumulates values through iteration, maintaining state between iterations through an accumulator parameter.
total = [10, 20, 30, 40].reduce(0) { |sum, num| sum + num }
# => 100
longest_word = %w[cat elephant dog].reduce do |longest, word|
word.length > longest.length ? word : longest
end
# => "elephant"
Advanced Usage
Method chaining combines multiple iteration methods to create complex data processing pipelines. Ruby evaluates chained methods left-to-right, with each method receiving the result of the previous method.
data = [
{ name: 'John', age: 25, department: 'engineering' },
{ name: 'Jane', age: 30, department: 'marketing' },
{ name: 'Bob', age: 22, department: 'engineering' },
{ name: 'Alice', age: 28, department: 'design' }
]
engineering_names = data
.select { |person| person[:department] == 'engineering' }
.map { |person| person[:name].upcase }
.sort
# => ["BOB", "JOHN"]
The each_with_index
method provides access to both element and index during iteration, while each_with_object
allows accumulation into a provided object.
# Index tracking during mapping
indexed_data = ['a', 'b', 'c'].map.with_index do |letter, index|
"#{index}: #{letter}"
end
# => ["0: a", "1: b", "2: c"]
# Accumulation with custom object
grouped = [1, 2, 3, 4, 5].each_with_object({}) do |num, hash|
key = num.even? ? :even : :odd
hash[key] ||= []
hash[key] << num
end
# => {:odd=>[1, 3, 5], :even=>[2, 4]}
Ruby supports custom enumerable classes by including the Enumerable module and defining an each
method. This pattern extends iteration behavior to domain-specific classes.
class TodoList
include Enumerable
def initialize
@items = []
end
def add_item(item)
@items << item
end
def each
return enum_for(:each) unless block_given?
@items.each { |item| yield item }
end
end
todos = TodoList.new
todos.add_item("Buy groceries")
todos.add_item("Walk dog")
completed = todos.map { |item| "✓ #{item}" }
# => ["✓ Buy groceries", "✓ Walk dog"]
Pattern matching with iteration methods provides powerful data extraction capabilities, particularly when combined with Ruby's case expressions and destructuring.
pairs = [[:name, 'John'], [:age, 25], [:city, 'NYC']]
person_data = pairs.each_with_object({}) do |(key, value), hash|
case key
when :name then hash[:full_name] = value.upcase
when :age then hash[:age_group] = value < 30 ? :young : :mature
when :city then hash[:location] = value
end
end
# => {:full_name=>"JOHN", :age_group=>:young, :location=>"NYC"}
Performance & Memory
Lazy evaluation through the lazy
method creates enumerator chains that process elements on-demand rather than creating intermediate arrays. This approach reduces memory usage for large datasets and enables processing of infinite sequences.
# Eager evaluation - creates intermediate arrays
result = (1..1_000_000)
.map { |n| n * 2 }
.select { |n| n % 3 == 0 }
.take(5)
# Lazy evaluation - processes elements on-demand
result = (1..1_000_000)
.lazy
.map { |n| n * 2 }
.select { |n| n % 3 == 0 }
.take(5)
.to_a
# => [6, 12, 18, 24, 30]
Memory allocation patterns differ significantly between iteration methods. Methods like map
and select
create new arrays, while each
and reduce
avoid intermediate allocations.
# High memory allocation
large_array = Array.new(1_000_000) { rand(100) }
processed = large_array
.map { |n| n * 2 } # Allocates 1M element array
.select { |n| n > 50 } # Allocates ~500K element array
.sort # Additional allocation for sorting
# Lower memory allocation
processed = large_array.each_with_object([]) do |n, result|
doubled = n * 2
result << doubled if doubled > 50
end.sort! # In-place sort
Block complexity affects iteration performance significantly. Ruby evaluates blocks for each element, so expensive operations within blocks multiply across collection size.
# Expensive operation in block
users = User.all
enhanced_users = users.map do |user|
{
user: user,
posts: user.posts.count, # Database query per user
avatar: resize_image(user.avatar) # Image processing per user
}
end
# Optimized approach with batch operations
post_counts = Post.group(:user_id).count
enhanced_users = users.map do |user|
{
user: user,
posts: post_counts[user.id] || 0,
avatar: user.avatar # Process images separately in batch
}
end
Common Pitfalls
Modifying collections during iteration creates unpredictable behavior and potential infinite loops. Ruby's iteration methods traverse collections based on their state at iteration start, but modifications can disrupt this process.
# Dangerous - modifying array during iteration
numbers = [1, 2, 3, 4, 5]
numbers.each do |n|
numbers.delete(n) if n.even? # Skips elements
end
# => [1, 3, 5] but may skip elements unpredictably
# Safe approach - create new collection
numbers = [1, 2, 3, 4, 5]
filtered = numbers.reject { |n| n.even? }
# => [1, 3, 5]
# Safe in-place modification
numbers.delete_if { |n| n.even? }
Hash iteration order depends on insertion order in modern Ruby, but relying on this order for critical logic creates fragile code.
# Fragile - depends on hash order
config = { host: 'localhost', port: 3000, ssl: true }
connection_string = config.map { |k, v| "#{k}=#{v}" }.join('&')
# Robust - explicit ordering
required_order = [:host, :port, :ssl]
connection_string = required_order.map { |key| "#{key}=#{config[key]}" }.join('&')
Block variable shadowing occurs when block parameters share names with variables in outer scope, leading to unexpected modifications.
total = 100
numbers = [10, 20, 30]
# Dangerous - shadows outer variable
numbers.each do |total| # 'total' shadows outer 'total'
puts total # Prints array elements, not outer total
end
puts total # Still 100, but confusing
# Clear - different variable names
numbers.each do |number|
puts number
total += number # Clearly references outer total
end
Enumerator state management requires attention to external iteration patterns. Enumerators maintain internal state that persists between method calls.
enum = [1, 2, 3].each
puts enum.next # => 1
puts enum.next # => 2
# Rewind to reset state
enum.rewind
puts enum.next # => 1 again
# Multiple enumerators maintain separate state
enum1 = [1, 2, 3].each
enum2 = [1, 2, 3].each
puts enum1.next # => 1
puts enum2.next # => 1 (independent state)
Nil handling in iteration requires explicit checking to avoid NoMethodError exceptions during element processing.
mixed_data = ['hello', nil, 'world', nil, 'ruby']
# Dangerous - nil.upcase raises NoMethodError
# results = mixed_data.map { |item| item.upcase }
# Safe approaches
results = mixed_data.map { |item| item&.upcase }
# => ["HELLO", nil, "WORLD", nil, "RUBY"]
results = mixed_data.filter_map { |item| item&.upcase }
# => ["HELLO", "WORLD", "RUBY"]
Reference
Core Enumerable Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each |
{block} |
self |
Executes block for each element |
#map / #collect |
{block} |
Array |
Transforms elements with block results |
#select / #filter |
{block} |
Array |
Keeps elements where block returns truthy |
#reject |
{block} |
Array |
Keeps elements where block returns falsy |
#reduce / #inject |
initial=nil, {block} |
Object |
Accumulates values through iteration |
#find / #detect |
{block} |
Object or nil |
Returns first element matching block |
#find_all |
{block} |
Array |
Returns all elements matching block |
#any? |
{block} |
Boolean |
Returns true if any element matches block |
#all? |
{block} |
Boolean |
Returns true if all elements match block |
#none? |
{block} |
Boolean |
Returns true if no elements match block |
Index and Object Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each_with_index |
{block} |
self |
Yields element and index to block |
#each_with_object(obj) |
obj, {block} |
obj |
Yields element and object to block |
#map.with_index |
{block} |
Array |
Maps with element and index |
#filter_map |
{block} |
Array |
Maps and filters nil values |
#each_slice(n) |
n, {block} |
self |
Yields successive slices of n elements |
#each_cons(n) |
n, {block} |
self |
Yields successive overlapping slices |
Utility Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#count |
obj=nil, {block} |
Integer |
Counts matching elements |
#group_by |
{block} |
Hash |
Groups elements by block results |
#partition |
{block} |
Array[Array, Array] |
Splits into two arrays based on block |
#sort |
{block} |
Array |
Sorts elements with optional comparison |
#sort_by |
{block} |
Array |
Sorts by block results |
#min / #max |
{block} |
Object |
Finds minimum/maximum element |
#min_by / #max_by |
{block} |
Object |
Finds min/max by block result |
#minmax |
{block} |
Array[min, max] |
Returns minimum and maximum |
Hash-Specific Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each_key |
{block} |
self |
Iterates over hash keys |
#each_value |
{block} |
self |
Iterates over hash values |
#each_pair |
{block} |
self |
Iterates over key-value pairs |
#transform_keys |
{block} |
Hash |
Creates hash with transformed keys |
#transform_values |
{block} |
Hash |
Creates hash with transformed values |
Enumerator Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#lazy |
None | Enumerator::Lazy |
Creates lazy enumerator |
#with_index(offset=0) |
offset=0 |
Enumerator |
Adds index to enumerator |
#with_object(obj) |
obj |
Enumerator |
Adds object to enumerator |
#next |
None | Object |
Returns next element |
#peek |
None | Object |
Returns next element without advancing |
#rewind |
None | self |
Resets enumerator position |
Common Block Patterns
# Conditional selection
numbers.select { |n| n > 10 && n.even? }
# Multiple transformations
words.map(&:strip).map(&:downcase).uniq
# Accumulator with index
items.each_with_index.reduce({}) { |hash, (item, i)| hash[i] = item; hash }
# Nested iteration
matrix.each_with_index do |row, i|
row.each_with_index { |cell, j| puts "#{i},#{j}: #{cell}" }
end