CrackedRuby logo

CrackedRuby

Iteration Methods

Ruby iteration methods provide flexible ways to process collections and enumerate through data structures.

Core Modules Enumerable Module
3.2.1

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