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
Core Iteration Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each { |item| } |
block | Array |
Executes block for each element, returns original array |
#each_with_index { |item, idx| } |
block | Array |
Yields element and index to block |
#each_with_object(obj) { |item, obj| } |
object, block | Object |
Accumulates into provided object |
#map { |item| } |
block | Array |
Transforms each element through block |
#map! { |item| } |
block | Array |
Transforms elements in place |
#filter_map { |item| } |
block | Array |
Maps and filters nil results in one pass |
Filtering Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#select { |item| } |
block | Array |
Returns elements where block is truthy |
#select! { |item| } |
block | Array or nil |
Filters in place, returns nil if unchanged |
#reject { |item| } |
block | Array |
Returns elements where block is falsy |
#reject! { |item| } |
block | Array or nil |
Rejects in place, returns nil if unchanged |
#filter { |item| } |
block | Array |
Alias for select |
#keep_if { |item| } |
block | Array |
Alias for select! |
Search Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#find { |item| } |
block | Object or nil |
Returns first matching element |
#find_index { |item| } |
block | Integer or nil |
Returns index of first match |
#index(obj) |
object | Integer or nil |
Returns index of object |
#rindex(obj) |
object | Integer or nil |
Returns last index of object |
#include?(obj) |
object | Boolean |
Tests if array contains object |
Boolean Test Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#all? { |item| } |
block (optional) | Boolean |
True if block returns truthy for all elements |
#any? { |item| } |
block (optional) | Boolean |
True if block returns truthy for any element |
#none? { |item| } |
block (optional) | Boolean |
True if block returns falsy for all elements |
#one? { |item| } |
block (optional) | Boolean |
True if block returns truthy for exactly one element |
Reduction Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#reduce(init) { |acc, item| } |
initial value (optional), block | Object |
Reduces elements to single value |
#inject(init) { |acc, item| } |
initial value (optional), block | Object |
Alias for reduce |
#sum(init) |
initial value (optional) | Numeric |
Calculates sum of elements |
#min |
none | Object or nil |
Returns minimum element |
#max |
none | Object or nil |
Returns maximum element |
#minmax |
none | Array |
Returns [min, max] pair |
Grouping and Partitioning
Method | Parameters | Returns | Description |
---|---|---|---|
#group_by { |item| } |
block | Hash |
Groups elements by block return value |
#partition { |item| } |
block | Array |
Returns [selected, rejected] arrays |
#slice_when { |prev, curr| } |
block | Enumerator |
Slices array when block returns truthy |
#chunk { |item| } |
block | Enumerator |
Groups consecutive elements with same block value |
#chunk_while { |prev, curr| } |
block | Enumerator |
Groups while block returns truthy |
Lazy Evaluation
Method | Parameters | Returns | Description |
---|---|---|---|
#lazy |
none | Enumerator::Lazy |
Creates lazy enumerator |
#force |
none | Array |
Forces evaluation of lazy enumerator |
Common Symbol-to-Proc Shortcuts
Symbol | Equivalent Block | Description |
---|---|---|
&:upcase |
{ |x| x.upcase } |
Calls upcase method |
&:downcase |
{ |x| x.downcase } |
Calls downcase method |
&:to_s |
{ |x| x.to_s } |
Converts to string |
&:to_i |
{ |x| x.to_i } |
Converts to integer |
&:even? |
{ |x| x.even? } |
Tests if even |
&:odd? |
{ |x| x.odd? } |
Tests if odd |
&:nil? |
{ |x| x.nil? } |
Tests if nil |
&:empty? |
{ |x| x.empty? } |
Tests if empty |
Performance Characteristics
Operation Type | Memory Usage | CPU Impact | Notes |
---|---|---|---|
each |
O(1) | O(n) | No new arrays created |
map |
O(n) | O(n) | Creates new array |
select/reject |
O(k) where k≤n | O(n) | Size depends on filtering |
reduce |
O(1) | O(n) | Single accumulated value |
Method chaining | O(n) per method | O(n) per method | Multiple intermediate arrays |
Lazy evaluation | O(1) until force | Deferred | No intermediate arrays |