CrackedRuby logo

CrackedRuby

Array Iteration Methods

A comprehensive guide to Ruby's array iteration methods covering core enumerable methods, performance characteristics, and common implementation patterns.

Core Built-in Classes Array Class
2.4.3

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 array
  • each_with_index - Yields element and index to block
  • each_with_object(obj) - Accumulates into provided object

Transformation

  • map - Creates new array with transformed elements
  • map! - Transforms elements in place
  • filter_map - Maps and filters nil results in one pass

Filtering

  • select - Returns elements where block is truthy
  • reject - Returns elements where block is falsy
  • select! / reject! - In-place versions (return nil if unchanged)

Search & Test

  • find - Returns first matching element
  • find_index - Returns index of first match
  • include?(obj) - Tests if array contains object
  • all? - True if block returns truthy for all elements
  • any? - True if block returns truthy for any element
  • none? - True if block returns falsy for all elements

Reduction

  • reduce(initial_value) - Reduces elements to single value
  • sum - Calculates sum of elements
  • min / 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 arrays
  • lazy - Deferred evaluation
  • find - 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