CrackedRuby logo

CrackedRuby

Array Iteration Methods

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