Overview
Ruby arrays provide iteration methods through the Enumerable module, offering declarative approaches to data transformation and filtering. These methods replace traditional for-loops with functional programming constructs that emphasize data flow and immutability principles.
The core iteration methods operate on array elements sequentially, applying blocks of code to transform, filter, or accumulate values. Methods like each
, map
, select
, and reduce
form the foundation of Ruby's collection processing capabilities.
# Basic iteration pattern
[1, 2, 3, 4].each { |n| puts n * 2 }
# 2
# 4
# 6
# 8
# Transformation with map
[1, 2, 3, 4].map { |n| n * 2 }
# => [2, 4, 6, 8]
# Filtering with select
[1, 2, 3, 4, 5, 6].select(&:even?)
# => [2, 4, 6]
Array iteration methods return new arrays or computed values without modifying the original array, supporting immutable programming patterns. Methods that end with exclamation marks (select!
, reject!
) modify the receiver array in place.
The iteration methods integrate with Ruby's block syntax, accepting both brace notation { }
for single-line operations and do..end
for multi-line blocks. Block parameters receive individual array elements, enabling element-specific processing logic.
Basic Usage
The each
method provides the fundamental iteration mechanism, executing a block for every array element. Unlike map
, each
returns the original array and focuses on side effects rather than transformation.
users = ['alice', 'bob', 'charlie']
users.each do |user|
puts "Processing user: #{user}"
# Perform side effects like logging, database updates
end
# Processing user: alice
# Processing user: bob
# Processing user: charlie
# => ['alice', 'bob', 'charlie']
The map
method transforms each element through the provided block, collecting results into a new array. This method implements the functional map operation, maintaining a one-to-one correspondence between input and output elements.
# String transformation
words = ['hello', 'world', 'ruby']
capitalized = words.map(&:capitalize)
# => ['Hello', 'World', 'Ruby']
# Numeric calculations with index
numbers = [10, 20, 30]
indexed_values = numbers.map.with_index { |num, idx| num + idx }
# => [10, 21, 32]
Filtering methods select
and reject
create new arrays based on boolean conditions. select
retains elements where the block returns truthy values, while reject
excludes them.
scores = [85, 92, 78, 96, 88, 91]
# Select high scores
high_scores = scores.select { |score| score >= 90 }
# => [92, 96, 91]
# Reject failing scores
passing_scores = scores.reject { |score| score < 80 }
# => [85, 92, 96, 88, 91]
# Complex filtering with multiple conditions
valid_scores = scores.select do |score|
score.between?(0, 100) && score >= 70
end
The find
method returns the first element matching the block condition, short-circuiting on the first match rather than processing the entire array.
inventory = [
{ item: 'laptop', price: 1200, stock: 5 },
{ item: 'mouse', price: 25, stock: 50 },
{ item: 'keyboard', price: 80, stock: 0 }
]
# Find first out of stock item
out_of_stock = inventory.find { |product| product[:stock] == 0 }
# => { item: 'keyboard', price: 80, stock: 0 }
# Find item by name
laptop = inventory.find { |product| product[:item] == 'laptop' }
Reduction operations with reduce
(alias inject
) accumulate array elements into a single value through iterative computation. The method accepts an optional initial value and combines elements using the block operation.
# Sum calculation
numbers = [1, 2, 3, 4, 5]
total = numbers.reduce(0) { |sum, num| sum + num }
# => 15
# Product calculation
product = numbers.reduce(1, :*)
# => 120
# Building hash from array
words = ['apple', 'banana', 'cherry']
word_lengths = words.reduce({}) do |hash, word|
hash[word] = word.length
hash
end
# => {'apple' => 5, 'banana' => 6, 'cherry' => 6}
Advanced Usage
Method chaining combines multiple iteration operations into expressive data transformation pipelines. Each method in the chain receives the output of the previous operation as input, enabling complex processing flows.
# Complex data transformation pipeline
sales_data = [
{ product: 'laptop', price: 1200, quantity: 2, category: 'electronics' },
{ product: 'book', price: 15, quantity: 5, category: 'media' },
{ product: 'phone', price: 800, quantity: 1, category: 'electronics' },
{ product: 'desk', price: 300, quantity: 1, category: 'furniture' }
]
electronics_revenue = sales_data
.select { |item| item[:category] == 'electronics' }
.map { |item| item[:price] * item[:quantity] }
.reduce(0, :+)
# => 3200
# Group and aggregate operations
category_totals = sales_data
.group_by { |item| item[:category] }
.transform_values do |items|
items.sum { |item| item[:price] * item[:quantity] }
end
# => {"electronics"=>3200, "media"=>75, "furniture"=>300}
The each_with_object
method provides an alternative to reduce
when building complex data structures, passing a mutable object through the iteration rather than accumulating through block return values.
# Building nested structures
transactions = [
{ date: '2024-01-15', amount: 100, type: 'credit' },
{ date: '2024-01-16', amount: 50, type: 'debit' },
{ date: '2024-01-15', amount: 200, type: 'credit' }
]
daily_summary = transactions.each_with_object({}) do |txn, summary|
date = txn[:date]
summary[date] ||= { credits: 0, debits: 0, count: 0 }
summary[date][:count] += 1
if txn[:type] == 'credit'
summary[date][:credits] += txn[:amount]
else
summary[date][:debits] += txn[:amount]
end
end
Complex filtering scenarios benefit from methods like partition
, which splits arrays based on conditions, and group_by
, which creates hash-based categorizations.
# Partition for binary classification
test_scores = [85, 92, 78, 96, 88, 91, 73, 89]
passing, failing = test_scores.partition { |score| score >= 80 }
# passing => [85, 92, 96, 88, 91, 89]
# failing => [78, 73]
# Multi-level grouping with nested operations
students = [
{ name: 'Alice', grade: 85, subject: 'math', semester: 'fall' },
{ name: 'Bob', grade: 92, subject: 'science', semester: 'fall' },
{ name: 'Charlie', grade: 78, subject: 'math', semester: 'spring' }
]
grade_analysis = students
.group_by { |s| s[:semester] }
.transform_values do |semester_students|
semester_students
.group_by { |s| s[:subject] }
.transform_values { |subject_students|
subject_students.map { |s| s[:grade] }.sum.fdiv(subject_students.size)
}
end
Custom enumerable objects integrate with array iteration patterns through duck typing, implementing each
and including the Enumerable module to gain access to transformation and filtering methods.
class NumberRange
include Enumerable
def initialize(start, finish, step = 1)
@start, @finish, @step = start, finish, step
end
def each
return enum_for(:each) unless block_given?
current = @start
while current <= @finish
yield current
current += @step
end
end
end
# Custom enumerable works with standard methods
range = NumberRange.new(1, 20, 3)
evens = range.select(&:even?) # => [4, 10, 16]
doubled = range.map { |n| n * 2 } # => [2, 8, 14, 20, 26, 32, 38]
Performance & Memory
Array iteration methods create intermediate objects that impact memory usage and processing speed. Method chaining generates temporary arrays between operations, increasing memory allocation and garbage collection overhead.
# Multiple intermediate arrays created
large_dataset = (1..1_000_000).to_a
result = large_dataset
.map { |n| n * 2 } # Creates 1M element array
.select { |n| n.even? } # Creates another filtered array
.map { |n| n.to_s } # Creates final string array
# Memory-efficient alternative using lazy evaluation
result = large_dataset.lazy
.map { |n| n * 2 }
.select { |n| n.even? }
.map { |n| n.to_s }
.force # Materializes the result only once
The lazy
enumerator defers computation until explicitly forced, processing elements one at a time through the entire chain rather than creating intermediate collections. This approach significantly reduces memory usage for large datasets.
# Lazy evaluation for infinite sequences
fibonacci = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end
# Only computes needed values
first_ten_even_fibs = fibonacci.lazy
.select(&:even?)
.take(10)
.force
# => [0, 2, 8, 34, 144, 610, 2584, 10946, 46368, 196418]
Block complexity affects iteration performance, with simple operations like arithmetic significantly outperforming complex string manipulation or object instantiation within blocks.
require 'benchmark'
data = (1..100_000).to_a
# Fast: simple arithmetic
Benchmark.measure do
data.map { |n| n * 2 }
end
# => ~0.01 seconds
# Slower: string operations
Benchmark.measure do
data.map { |n| "number_#{n}_formatted" }
end
# => ~0.08 seconds
# Slowest: object instantiation
Benchmark.measure do
data.map { |n| Time.at(n) }
end
# => ~0.35 seconds
Symbol-to-proc notation (&:method_name
) provides performance benefits for simple method calls by avoiding block creation overhead. This optimization works best with single method invocations on elements.
# Performance comparison for 1M strings
strings = Array.new(1_000_000) { 'hello' }
Benchmark.compare do |bm|
bm.report('block') { strings.map { |s| s.upcase } }
bm.report('symbol') { strings.map(&:upcase) }
end
# symbol notation typically 10-20% faster
In-place modification methods (map!
, select!
) reduce memory allocation by modifying the receiver array directly, avoiding new array creation. However, these methods sacrifice immutability and can complicate debugging and testing.
# Memory usage comparison
original = (1..1_000_000).to_a.dup
# Creates new array - higher memory usage
doubled = original.map { |n| n * 2 }
# Modifies in place - lower memory usage but mutates original
original.map! { |n| n * 2 }
Early termination methods like find
, any?
, and all?
provide performance advantages by stopping iteration when the condition is satisfied, rather than processing all elements.
# Performance difference with early termination
large_array = (1..1_000_000).to_a
# Inefficient: processes entire array even after finding match
has_even = large_array.select(&:even?).any?
# Efficient: stops at first even number (index 1)
has_even = large_array.any?(&:even?)
# Benchmark shows dramatic difference for early matches
Common Pitfalls
Modifying arrays during iteration produces undefined behavior and often leads to skipped elements or infinite loops. Ruby's iteration methods use internal pointers that become inconsistent when the underlying array changes during processing.
# Dangerous: modifying during iteration
numbers = [1, 2, 3, 4, 5]
numbers.each do |num|
numbers.delete(num) if num.even? # Skips elements unpredictably
end
# Safe: collect modifications separately
to_delete = []
numbers.each do |num|
to_delete << num if num.even?
end
to_delete.each { |num| numbers.delete(num) }
# Better: use non-mutating methods
odds_only = numbers.reject(&:even?)
Block variable naming conflicts arise when iteration methods nest or when block parameters shadow local variables. Ruby resolves variable names based on lexical scoping rules, potentially accessing unintended variables.
# Confusing variable shadowing
result = 10
numbers = [1, 2, 3]
# 'result' parameter shadows local variable
sum = numbers.reduce(0) { |result, num| result + num }
puts result # Still 10, not affected by block
# Clearer naming prevents confusion
sum = numbers.reduce(0) { |accumulator, num| accumulator + num }
# Nested iteration with clear variable names
matrix = [[1, 2], [3, 4], [5, 6]]
flattened = matrix.map do |row|
row.map { |cell| cell * 2 } # Distinct row vs cell naming
end.flatten
Expecting mutations from non-mutating methods causes logic errors when developers assume methods like map
and select
modify the original array. These methods return new arrays while leaving the original unchanged.
# Common mistake: expecting mutation
scores = [85, 92, 78]
scores.map { |score| score + 5 } # Returns new array
puts scores # Still [85, 92, 78] - original unchanged
# Correct approaches
adjusted_scores = scores.map { |score| score + 5 } # Assignment
scores = scores.map { |score| score + 5 } # Reassignment
scores.map! { |score| score + 5 } # Explicit mutation
Return value confusion occurs when methods like each
return the original array rather than the block results, leading to unexpected values in method chains or assignments.
# Unexpected return value
numbers = [1, 2, 3]
doubled = numbers.each { |n| n * 2 }
# doubled is [1, 2, 3], not [2, 4, 6]
# each always returns original array regardless of block
# Use map for transformation
doubled = numbers.map { |n| n * 2 } # => [2, 4, 6]
Nil handling in iteration methods can cause NoMethodError when array elements are nil and the block attempts method calls without checking.
# Dangerous: nil elements cause crashes
mixed_data = ['hello', nil, 'world', nil]
upcased = mixed_data.map(&:upcase) # NoMethodError on nil
# Safe approaches
upcased = mixed_data.map { |str| str&.upcase || '' }
upcased = mixed_data.compact.map(&:upcase) # Remove nils first
upcased = mixed_data.filter_map(&:upcase) # Map and filter nils
Block parameter arity mismatches occur when iteration methods yield different numbers of arguments than the block expects, causing unused parameters or missing values.
nested_data = [['a', 1], ['b', 2], ['c', 3]]
# Single parameter receives entire sub-array
nested_data.each { |item| puts item.inspect }
# => ['a', 1], ['b', 2], ['c', 3]
# Multiple parameters automatically destructure
nested_data.each { |key, value| puts "#{key}: #{value}" }
# => a: 1, b: 2, c: 3
# Excess parameters become nil
nested_data.each { |key, value, extra| puts extra.inspect }
# => nil, nil, nil
Reference
Essential Methods
Basic Iteration
each
- Executes block for each element, returns original arrayeach_with_index
- Yields element and index to blockeach_with_object(obj)
- Accumulates into provided object
Transformation
map
- Creates new array with transformed elementsmap!
- Transforms elements in placefilter_map
- Maps and filters nil results in one pass
Filtering
select
- Returns elements where block is truthyreject
- Returns elements where block is falsyselect!
/reject!
- In-place versions (return nil if unchanged)
Search & Test
find
- Returns first matching elementfind_index
- Returns index of first matchinclude?(obj)
- Tests if array contains objectall?
- True if block returns truthy for all elementsany?
- True if block returns truthy for any elementnone?
- True if block returns falsy for all elements
Reduction
reduce(initial_value)
- Reduces elements to single valuesum
- Calculates sum of elementsmin
/max
- Returns minimum/maximum element
Method Signatures
# Core patterns
array.each { |element| ... } # => original array
array.map { |element| new_value } # => new array
array.select { |element| condition } # => filtered array
array.find { |element| condition } # => first match or nil
array.reduce(initial) { |acc, el| ... } # => accumulated value
# With index
array.each_with_index { |element, index| ... }
array.map.with_index { |element, index| ... }
# Multiple parameters (for nested arrays)
pairs.each { |key, value| ... }
matrix.each { |row| row.each { |cell| ... } }
Symbol-to-Proc Shortcuts
# Instead of blocks, use symbols for simple method calls
strings.map(&:upcase) # Same as: strings.map { |s| s.upcase }
numbers.select(&:even?) # Same as: numbers.select { |n| n.even? }
objects.map(&:to_s) # Same as: objects.map { |o| o.to_s }
# Common shortcuts
&:downcase, &:capitalize, &:strip
&:to_i, &:to_f, &:to_sym
&:odd?, &:even?, &:nil?, &:empty?
&:reverse, &:sort, &:uniq
Return Values Quick Reference
Method | Returns | Mutates Original? |
---|---|---|
each |
Original array | No |
map |
New array | No |
select |
New array | No |
find |
Element or nil | No |
reduce |
Accumulated value | No |
map! |
Modified array | Yes |
select! |
Modified array or nil | Yes |
Performance Notes
Memory Efficient
each
- No intermediate arrayslazy
- Deferred evaluationfind
- Stops at first match
Memory Intensive
map
+select
chains - Multiple intermediate arrays- Large transformations without
lazy
CPU Efficient
- Symbol-to-proc (
&:method
) for simple operations - Early termination methods (
find
,any?
,all?
)
Avoid
- Modifying arrays during iteration
- Complex object creation in tight loops