Overview
Ruby provides enumeration capabilities through the Enumerable
module, which defines iteration methods that work with any class implementing #each
. Arrays, hashes, ranges, and custom objects can include this module to gain powerful collection processing methods.
The Enumerable
module contains over 50 methods for filtering, transforming, and aggregating data. Core methods include #map
, #select
, #reject
, #reduce
, and #each_with_index
. Ruby also provides Enumerator
objects for lazy evaluation and custom iteration patterns.
# Basic enumeration with Array
numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { |n| n * 2 }
# => [2, 4, 6, 8, 10]
# Hash enumeration
scores = { alice: 95, bob: 87, carol: 92 }
passing = scores.select { |name, score| score >= 90 }
# => { alice: 95, carol: 92 }
Classes gain enumeration by including Enumerable
and defining #each
:
class WordList
include Enumerable
def initialize(words)
@words = words
end
def each
@words.each { |word| yield word.upcase }
end
end
list = WordList.new(['hello', 'world'])
list.map(&:reverse)
# => ["OLLEH", "DLROW"]
Basic Usage
Enumeration methods accept blocks that define processing logic. The #each
method iterates without returning a new collection, while transformation methods like #map
and #select
return modified collections.
Collection filtering uses #select
for inclusion and #reject
for exclusion:
ages = [15, 22, 8, 35, 19, 67]
adults = ages.select { |age| age >= 18 }
# => [22, 35, 19, 67]
minors = ages.reject { |age| age >= 18 }
# => [15, 8]
The #map
method transforms each element:
names = ['alice', 'bob', 'carol']
capitalized = names.map(&:capitalize)
# => ["Alice", "Bob", "Carol"]
lengths = names.map(&:length)
# => [5, 3, 5]
Aggregation methods reduce collections to single values. The #reduce
method (aliased as #inject
) accumulates results:
# Sum calculation
total = [1, 2, 3, 4, 5].reduce(0) { |sum, n| sum + n }
# => 15
# String concatenation
sentence = %w[Ruby makes enumeration easy].reduce { |result, word| "#{result} #{word}" }
# => "Ruby makes enumeration easy"
# Hash building
word_counts = %w[apple banana apple cherry banana apple].reduce(Hash.new(0)) do |counts, word|
counts[word] += 1
counts
end
# => {"apple"=>3, "banana"=>2, "cherry"=>1}
The #find
method returns the first matching element, while #find_all
is an alias for #select
:
users = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Carol', age: 35 }
]
first_adult = users.find { |user| user[:age] >= 30 }
# => { name: 'Alice', age: 30 }
Advanced Usage
Method chaining creates processing pipelines by combining multiple enumeration operations. Ruby evaluates chains eagerly by default, creating intermediate collections:
data = [
{ name: 'Alice', department: 'Engineering', salary: 95000 },
{ name: 'Bob', department: 'Marketing', salary: 65000 },
{ name: 'Carol', department: 'Engineering', salary: 87000 },
{ name: 'David', department: 'Sales', salary: 72000 }
]
# Complex chain processing
high_earners = data
.select { |emp| emp[:salary] > 70000 }
.group_by { |emp| emp[:department] }
.transform_values { |emps| emps.map { |emp| emp[:name] } }
# => {"Engineering"=>["Alice", "Carol"], "Sales"=>["David"]}
The Enumerator
class enables lazy evaluation and custom iteration patterns. Lazy enumerators process elements on-demand rather than creating intermediate collections:
# Infinite sequence with lazy evaluation
fibonacci = Enumerator.new do |yielder|
a, b = 0, 1
loop do
yielder << a
a, b = b, a + b
end
end.lazy
first_ten = fibonacci.take(10).to_a
# => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
# Large file processing without memory overhead
def process_large_file(filename)
File.foreach(filename).lazy
.map(&:chomp)
.select { |line| line.match?(/ERROR/) }
.map { |line| parse_log_entry(line) }
.take(100)
.to_a
end
Custom enumerators handle specialized iteration requirements:
class TreeNode
include Enumerable
def initialize(value, children = [])
@value = value
@children = children
end
# Depth-first traversal
def each(&block)
return enum_for(:each) unless block_given?
yield @value
@children.each { |child| child.each(&block) }
end
# Breadth-first traversal
def breadth_first
return enum_for(:breadth_first) unless block_given?
queue = [self]
until queue.empty?
node = queue.shift
yield node.value
queue.concat(node.children)
end
end
attr_reader :value, :children
end
tree = TreeNode.new('root', [
TreeNode.new('child1', [TreeNode.new('grandchild1')]),
TreeNode.new('child2')
])
tree.map(&:upcase)
# => ["ROOT", "CHILD1", "GRANDCHILD1", "CHILD2"]
tree.breadth_first.to_a
# => ["root", "child1", "child2", "grandchild1"]
The #group_by
method creates hash partitions, while #partition
splits into two arrays:
# Grouping by multiple criteria
transactions = [
{ amount: 100, type: 'credit', date: '2023-01-01' },
{ amount: 50, type: 'debit', date: '2023-01-01' },
{ amount: 200, type: 'credit', date: '2023-01-02' }
]
by_date_and_type = transactions.group_by { |t| [t[:date], t[:type]] }
# => {["2023-01-01", "credit"]=>[{:amount=>100, :type=>"credit", :date=>"2023-01-01"}], ...}
# Binary partition
credits, debits = transactions.partition { |t| t[:type] == 'credit' }
Performance & Memory
Enumeration method selection significantly impacts performance. Lazy evaluation prevents unnecessary computation and reduces memory usage for large datasets:
# Eager evaluation - processes entire collection
def find_expensive_items_eager(products)
products
.map { |p| expensive_calculation(p) } # Processes all items
.select { |result| result[:score] > 0.8 }
.first
end
# Lazy evaluation - stops at first match
def find_expensive_items_lazy(products)
products.lazy
.map { |p| expensive_calculation(p) } # Processes until match found
.select { |result| result[:score] > 0.8 }
.first
end
Some methods create full result arrays, while others can short-circuit:
# Memory-intensive operations
large_array = (1..1_000_000).to_a
# Creates intermediate arrays
result1 = large_array.map { |n| n * 2 }.select { |n| n > 500_000 }.first(10)
# Memory-efficient with lazy evaluation
result2 = large_array.lazy.map { |n| n * 2 }.select { |n| n > 500_000 }.first(10)
# Short-circuiting methods
any_large = large_array.any? { |n| n > 900_000 } # Stops at first match
all_positive = large_array.all? { |n| n > 0 } # Stops at first false
Hash operations generally perform better than array operations for lookup-heavy enumeration:
# Inefficient - repeated array searches
def categorize_slow(items, categories)
items.map do |item|
category = categories.find { |cat| cat[:id] == item[:category_id] }
{ item: item, category: category }
end
end
# Efficient - hash lookup
def categorize_fast(items, categories)
category_hash = categories.index_by { |cat| cat[:id] }
items.map do |item|
{ item: item, category: category_hash[item[:category_id]] }
end
end
Common Pitfalls
Block variable scope can cause unexpected behavior when blocks modify outer variables:
# Dangerous - modifying collection during enumeration
items = [1, 2, 3, 4, 5]
items.each do |item|
items.delete(item) if item.even? # Skips elements
end
# => [1, 3, 5] but may skip elements due to shifting indices
# Safe approach
items = [1, 2, 3, 4, 5]
items = items.reject(&:even?)
The difference between #map
and #each
confuses developers. Use #each
for side effects and #map
for transformations:
# Wrong - using each for transformation
names = []
['alice', 'bob'].each { |name| names << name.capitalize }
# Correct - using map for transformation
names = ['alice', 'bob'].map(&:capitalize)
# Wrong - using map for side effects
['alice', 'bob'].map { |name| puts name.capitalize } # Returns array of nils
# Correct - using each for side effects
['alice', 'bob'].each { |name| puts name.capitalize }
Symbol-to-proc conversion (&:method
) has limitations with method arguments:
# Works - no arguments
numbers = [1, 2, 3]
strings = numbers.map(&:to_s)
# Fails - method requires arguments
strings = ['hello', 'world']
# strings.map(&:ljust(10)) # ArgumentError
# Correct - use explicit block
padded = strings.map { |s| s.ljust(10) }
Hash enumeration yields key-value pairs as separate block parameters:
scores = { alice: 95, bob: 87 }
# Wrong - treats each pair as single argument
# scores.map { |pair| pair * 2 } # NoMethodError
# Correct - separate key and value parameters
doubled = scores.map { |name, score| [name, score * 2] }.to_h
# => { alice: 190, bob: 174 }
Production Patterns
Database record processing benefits from batching and lazy evaluation to avoid memory exhaustion:
# Process large result sets in batches
class ReportGenerator
def generate_user_report
User.find_each(batch_size: 1000) do |user|
process_user_data(user)
end
end
def calculate_metrics(users)
users.lazy
.select(&:active?)
.map { |user| UserMetrics.new(user) }
.each { |metrics| store_metrics(metrics) }
end
end
API response processing requires error handling within enumeration blocks:
class APIDataProcessor
def process_api_responses(urls)
results = []
errors = []
urls.each_with_index do |url, index|
begin
response = fetch_api_data(url)
parsed = JSON.parse(response.body)
results << transform_response(parsed)
rescue StandardError => error
errors << { url: url, index: index, error: error.message }
end
end
{ results: results, errors: errors }
end
end
Configuration processing with enumeration handles nested data structures:
class ConfigProcessor
def process_environment_config(config)
config.each_with_object({}) do |(key, value), processed|
processed_key = key.to_s.downcase.gsub('-', '_')
processed[processed_key] = case value
when Hash
process_environment_config(value)
when Array
value.map { |item| item.is_a?(Hash) ? process_environment_config(item) : item }
else
value
end
end
end
end
Log processing with enumeration handles large files efficiently:
class LogAnalyzer
def analyze_access_logs(log_file)
stats = Hash.new { |h, k| h[k] = Hash.new(0) }
File.foreach(log_file).lazy
.map(&:chomp)
.filter_map { |line| parse_log_line(line) }
.group_by { |entry| entry[:hour] }
.each do |hour, entries|
stats[hour][:requests] = entries.size
stats[hour][:unique_ips] = entries.map { |e| e[:ip] }.uniq.size
stats[hour][:status_codes] = entries.group_by { |e| e[:status] }
.transform_values(&:size)
end
stats
end
end
Reference
Core Enumerable Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#each |
&block |
Enumerable |
Yields each element to block |
#map |
&block |
Array |
Transforms each element via block |
#select |
&block |
Array |
Returns elements where block is truthy |
#reject |
&block |
Array |
Returns elements where block is falsy |
#find |
&block |
Object or nil |
Returns first element where block is truthy |
#reduce |
initial=nil, &block |
Object |
Accumulates result via block |
#each_with_index |
&block |
Enumerable |
Yields element and index |
#each_with_object |
object, &block |
Object |
Yields element and object |
Filtering and Searching Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#filter |
&block |
Array |
Alias for select |
#find_all |
&block |
Array |
Alias for select |
#detect |
&block |
Object or nil |
Alias for find |
#grep |
pattern |
Array |
Elements matching pattern |
#grep_v |
pattern |
Array |
Elements not matching pattern |
Testing Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#all? |
&block |
Boolean |
True if block returns truthy for all |
#any? |
&block |
Boolean |
True if block returns truthy for any |
#none? |
&block |
Boolean |
True if block returns falsy for all |
#one? |
&block |
Boolean |
True if block returns truthy for exactly one |
#include? |
object |
Boolean |
True if collection includes object |
Grouping and Partitioning
Method | Parameters | Returns | Description |
---|---|---|---|
#group_by |
&block |
Hash |
Groups elements by block result |
#partition |
&block |
Array |
Splits into two arrays based on block |
#chunk |
&block |
Enumerator |
Groups consecutive elements |
#slice_when |
&block |
Enumerator |
Splits when block returns truthy |
Aggregation Methods
Method | Parameters | Returns | Description |
---|---|---|---|
#count |
object=nil, &block |
Integer |
Count of elements |
#sum |
init=0, &block |
Object |
Sum of elements |
#min |
&block |
Object |
Minimum element |
#max |
&block |
Object |
Maximum element |
#minmax |
&block |
Array |
Array of min and max |
Enumerator Class
Method | Parameters | Returns | Description |
---|---|---|---|
Enumerator.new |
&block |
Enumerator |
Creates custom enumerator |
#lazy |
Enumerator::Lazy |
Returns lazy enumerator | |
#with_index |
offset=0 |
Enumerator |
Adds index to enumeration |
#next |
Object |
Returns next element | |
#rewind |
Enumerator |
Resets enumerator position |
Common Block Patterns
# Symbol to proc conversion
collection.map(&:method_name)
collection.select(&:predicate?)
# Index access
collection.each_with_index { |item, index| }
collection.map.with_index { |item, index| }
# Hash iteration
hash.each { |key, value| }
hash.map { |key, value| [new_key, new_value] }
# Nested enumeration
matrix.each { |row| row.each { |cell| } }
matrix.flatten.each { |cell| }