Skip to content

Ruby Iterators

Iterators are a powerful and elegant feature in Ruby that provides a standardized way to iterate over collection elements. Ruby's iterators include both built-in iteration methods and support for custom iterators, making code more concise and expressive. This chapter will provide a detailed introduction to Ruby's iterator usage methods and best practices.

🎯 Iterator Basics

What is an Iterator

An iterator is a design pattern that provides a method for accessing collection elements without exposing the internal representation of the collection. In Ruby, iterators are typically implemented in the form of blocks, making code more concise and readable.

ruby
# Basic iterator usage
[1, 2, 3, 4, 5].each { |n| puts n }
# Output:
# 1
# 2
# 3
# 4
# 5

# Using do...end syntax
[1, 2, 3, 4, 5].each do |n|
  puts n
end

# Iterator with block parameters
fruits = ["apple", "banana", "orange"]
fruits.each_with_index do |fruit, index|
  puts "#{index + 1}. #{fruit}"
end
# Output:
# 1. apple
# 2. banana
# 3. orange

Built-in Iterator Methods

ruby
# each - basic iteration
[1, 2, 3].each { |n| puts n }

# map/collect - transform each element
squared = [1, 2, 3, 4].map { |n| n ** 2 }
puts squared.inspect  # [1, 4, 9, 16]

# select/find_all - filter elements that meet conditions
evens = [1, 2, 3, 4, 5, 6].select(&:even?)
puts evens.inspect  # [2, 4, 6]

# reject - exclude elements that meet conditions
odds = [1, 2, 3, 4, 5, 6].reject(&:even?)
puts odds.inspect  # [1, 3, 5]

# find/detect - find the first element that meets conditions
first_even = [1, 3, 4, 5, 6].find(&:even?)
puts first_even  # 4

# find_all - find all elements that meet conditions (same as select)
all_evens = [1, 2, 3, 4, 5, 6].find_all(&:even?)
puts all_evens.inspect  # [2, 4, 6]

🔁 Common Iterator Methods

Basic Iteration Methods

ruby
# each - perform operation on each element
numbers = [1, 2, 3, 4, 5]
numbers.each { |n| puts "Number: #{n}" }

# each_with_index - iteration with index
fruits = ["apple", "banana", "orange"]
fruits.each_with_index { |fruit, index| puts "#{index}: #{fruit}" }

# reverse_each - reverse iteration
[1, 2, 3].reverse_each { |n| puts n }
# Output: 3, 2, 1

# each_slice - iterate in chunks
(1..10).each_slice(3) { |slice| puts slice.inspect }
# [1, 2, 3]
# [4, 5, 6]
# [7, 8, 9]
# [10]

# each_cons - consecutive element iteration
(1..5).each_cons(3) { |cons| puts cons.inspect }
# [1, 2, 3]
# [2, 3, 4]
# [3, 4, 5]

Transformation Iterators

ruby
# map/collect - transform each element
numbers = [1, 2, 3, 4, 5]
squared = numbers.map { |n| n ** 2 }
puts squared.inspect  # [1, 4, 9, 16, 25]

# map! - in-place transformation
numbers.map! { |n| n * 2 }
puts numbers.inspect  # [2, 4, 6, 8, 10]

# flat_map - flatten mapping
nested = [[1, 2], [3, 4], [5, 6]]
flattened = nested.flat_map { |arr| arr }
puts flattened.inspect  # [1, 2, 3, 4, 5, 6]

# collect_concat - same as flat_map
result = nested.collect_concat { |arr| arr.map { |n| n * 2 } }
puts result.inspect  # [2, 4, 6, 8, 10, 12]

# zip - merge multiple arrays
letters = ["a", "b", "c"]
numbers = [1, 2, 3]
combined = letters.zip(numbers)
puts combined.inspect  # [["a", 1], ["b", 2], ["c", 3]]

letters.zip(numbers) { |letter, number| puts "#{letter}: #{number}" }
# a: 1
# b: 2
# c: 3

Filter Iterators

ruby
# select/find_all - select elements that meet conditions
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = numbers.select { |n| n.even? }
puts evens.inspect  # [2, 4, 6, 8, 10]

# reject - exclude elements that meet conditions
odds = numbers.reject { |n| n.even? }
puts odds.inspect  # [1, 3, 5, 7, 9]

# grep - filter using regular expressions
words = ["apple", "banana", "cherry", "date"]
a_words = words.grep(/^a/)
puts a_words.inspect  # ["apple"]

# grep_v - exclude elements matching regular expression
non_a_words = words.grep_v(/^a/)
puts non_a_words.inspect  # ["banana", "cherry", "date"]

# take_while - take elements from the start that meet conditions
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
small_numbers = numbers.take_while { |n| n < 5 }
puts small_numbers.inspect  # [1, 2, 3, 4]

# drop_while - skip elements from the start that meet conditions
large_numbers = numbers.drop_while { |n| n < 5 }
puts large_numbers.inspect  # [5, 6, 7, 8, 9, 10]

Search Iterators

ruby
# find/detect - find the first element that meets conditions
numbers = [1, 3, 5, 6, 7, 8]
first_even = numbers.find(&:even?)
puts first_even  # 6

# find_index - find the index of the first element that meets conditions
index = numbers.find_index(&:even?)
puts index  # 3

# any? - check if any element meets conditions
has_even = numbers.any?(&:even?)
puts has_even  # true

# all? - check if all elements meet conditions
all_even = numbers.all?(&:even?)
puts all_even  # false

# none? - check if no elements meet conditions
no_negative = numbers.none? { |n| n < 0 }
puts no_negative  # true

# one? - check if only one element meets conditions
one_even = numbers.one?(&:even?)
puts one_even  # false

# include?/member? - check if a specific element is included
has_five = numbers.include?(5)
puts has_five  # true

🔢 Numeric Iterators

Numeric Range Iteration

ruby
# times - iterate specified times starting from 0
5.times { |i| puts "Iteration ##{i + 1}" }
# Iteration #1
# Iteration #2
# Iteration #3
# Iteration #4
# Iteration #5

# upto - iterate upward from current number to specified number
1.upto(5) { |n| puts n }

# downto - iterate downward from current number to specified number
5.downto(1) { |n| puts n }

# step - iterate with specified step size
1.step(10, 2) { |n| puts n }  # 1, 3, 5, 7, 9

# step with block parameters
1.step(10, 2) { |n| puts "Number: #{n}" }

# Using range iteration
(1..5).each { |n| puts n }
('a'..'e').each { |char| puts char }

Numeric Accumulation Iteration

ruby
# reduce/inject - accumulation operation
numbers = [1, 2, 3, 4, 5]

# Sum
sum = numbers.reduce(0) { |total, n| total + n }
puts sum  # 15

# Using symbol shorthand
sum = numbers.reduce(0, :+)
puts sum  # 15

# Product
product = numbers.reduce(1) { |total, n| total * n }
puts product  # 120

# Find maximum
max = numbers.reduce { |max, n| n > max ? n : max }
puts max  # 5

# String concatenation
words = ["Hello", "World", "Ruby"]
sentence = words.reduce("") { |result, word| result + " " + word }.strip
puts sentence  # Hello World Ruby

# Build hash
pairs = [["name", "Alice"], ["age", "25"], ["city", "Beijing"]]
hash = pairs.reduce({}) do |result, pair|
  result[pair[0]] = pair[1]
  result
end
puts hash.inspect  # {"name"=>"Alice", "age"=>"25", "city"=>"Beijing"}

🔄 Advanced Iterators

Grouping and Partitioning

ruby
# group_by - group by condition
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
grouped = numbers.group_by { |n| n.even? ? "even" : "odd" }
puts grouped
# {"odd"=>[1, 3, 5, 7, 9], "even"=>[2, 4, 6, 8, 10]}

# partition - partition into two groups
partitioned = numbers.partition(&:even?)
puts partitioned.inspect
# [[2, 4, 6, 8, 10], [1, 3, 5, 7, 9]]

# chunk - split by continuous same condition
data = [1, 2, 4, 6, 7, 9, 10, 12]
chunks = data.chunk { |n| n.even? }.to_a
puts chunks.inspect
# [[true, [1]], [false, [2, 4, 6]], [true, [7]], [false, [9, 10, 12]]]

# slice_before - slice before condition is met
words = ["apple", "ant", "banana", "bee", "cherry", "cat"]
sliced = words.slice_before { |word| word.start_with?('a') }.to_a
puts sliced.inspect
# [["apple"], ["ant", "banana"], ["bee", "cherry"], ["cat"]]

# slice_after - slice after condition is met
sliced_after = words.slice_after { |word| word.end_with?('e') }.to_a
puts sliced_after.inspect
# [["apple", "ant"], ["banana", "bee", "cherry"], ["cat"]]

Sorting Iterators

ruby
# sort - sort
numbers = [3, 1, 4, 1, 5, 9, 2, 6]
sorted = numbers.sort
puts sorted.inspect  # [1, 1, 2, 3, 4, 5, 6, 9]

# sort_by - sort by specific condition
words = ["apple", "banana", "cherry", "date"]
by_length = words.sort_by(&:length)
puts by_length.inspect  # ["date", "apple", "cherry", "banana"]

# reverse - reverse
reversed = numbers.reverse
puts reversed.inspect  # [6, 2, 9, 5, 1, 4, 1, 3]

# shuffle - random order
shuffled = numbers.shuffle
puts shuffled.inspect  # Random order

# sample - random sampling
sampled = numbers.sample(3)
puts sampled.inspect  # Random 3 elements

🎯 Custom Iterators

Creating Custom Iterators

ruby
class CustomCollection
  def initialize(items)
    @items = items
  end
  
  # Basic iterator
  def each
    @items.each { |item| yield item }
  end
  
  # Iterator with index
  def each_with_index
    @items.each_with_index { |item, index| yield item, index }
  end
  
  # Custom map iterator
  def custom_map
    result = []
    each { |item| result << yield(item) }
    result
  end
  
  # Conditional iterator
  def each_if(&condition)
    each do |item|
      yield item if condition.call(item)
    end
  end
  
  # Reverse iterator
  def reverse_each
    (@items.length - 1).downto(0) do |i|
      yield @items[i]
    end
  end
end

# Using custom iterators
collection = CustomCollection.new([1, 2, 3, 4, 5])

# Basic iteration
collection.each { |n| puts n }

# Iteration with index
collection.each_with_index { |item, index| puts "#{index}: #{item}" }

# Custom mapping
squared = collection.custom_map { |n| n ** 2 }
puts squared.inspect  # [1, 4, 9, 16, 25]

# Conditional iteration
collection.each_if { |n| n.even? } { |n| puts "Even: #{n}" }

# Reverse iteration
collection.reverse_each { |n| puts "Reverse: #{n}" }

Enumerable Module

ruby
class NumberSequence
  include Enumerable
  
  def initialize(start, finish)
    @start = start
    @finish = finish
  end
  
  # Implement each method to get all Enumerable functionality
  def each
    (@start..@finish).each { |n| yield n }
  end
  
  # Can override specific methods for performance
  def size
    @finish - @start + 1
  end
end

# Using class with Enumerable
sequence = NumberSequence.new(1, 10)

# Now can use all Enumerable methods
puts sequence.map { |n| n * 2 }.inspect  # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
puts sequence.select(&:even?).inspect    # [2, 4, 6, 8, 10]
puts sequence.find { |n| n > 5 }         # 6
puts sequence.all? { |n| n > 0 }         # true
puts sequence.any? { |n| n > 15 }        # false
puts sequence.count                      # 10

🎯 Iterator Practice Examples

Data Processing Pipeline

ruby
class DataPipeline
  def initialize(data)
    @data = data
  end
  
  # Chained method calls
  def filter(&block)
    DataPipeline.new(@data.select(&block))
  end
  
  def map(&block)
    DataPipeline.new(@data.map(&block))
  end
  
  def sort(&block)
    DataPipeline.new(@data.sort(&block))
  end
  
  def take(n)
    DataPipeline.new(@data.take(n))
  end
  
  def group_by(&block)
    @data.group_by(&block)
  end
  
  def result
    @data
  end
end

# Using data processing pipeline
numbers = (1..20).to_a
result = DataPipeline.new(numbers)
  .filter { |n| n.even? }
  .map { |n| n ** 2 }
  .sort { |a, b| b <=> a }
  .take(5)
  .result

puts result.inspect  # [400, 256, 144, 64, 16]

File Line Processing

ruby
class FileProcessor
  def self.process_lines(filename, &block)
    File.open(filename, 'r') do |file|
      file.each_line.with_index(1) do |line, line_number|
        yield line.chomp, line_number
      end
    end
  rescue Errno::ENOENT
    puts "File #{filename} does not exist"
  rescue => e
    puts "Error processing file: #{e.message}"
  end
  
  def self.filter_lines(filename, pattern)
    lines = []
    process_lines(filename) do |line, line_number|
      lines << { line: line, number: line_number } if line.match?(pattern)
    end
    lines
  end
  
  def self.transform_lines(filename, &transformer)
    transformed = []
    process_lines(filename) do |line, line_number|
      transformed << { original: line, transformed: transformer.call(line), number: line_number }
    end
    transformed
  end
end

# Using file processor (assuming test.txt exists)
# filtered = FileProcessor.filter_lines('test.txt', /ruby/i)
# transformed = FileProcessor.transform_lines('test.txt') { |line| line.upcase }

Iterator Combinator

ruby
class IteratorCombinator
  # Combine multiple iterator operations
  def self.process(data, operations)
    result = data
    operations.each do |operation|
      case operation[:type]
      when :map
        result = result.map(&operation[:block])
      when :select
        result = result.select(&operation[:block])
      when :reject
        result = result.reject(&operation[:block])
      when :sort
        result = result.sort(&operation[:block])
      when :take
        result = result.take(operation[:count])
      end
    end
    result
  end
  
  # Create operation chain
  def self.operation_chain
    []
  end
  
  # Add map operation
  def self.add_map(chain, &block)
    chain << { type: :map, block: block }
  end
  
  # Add select operation
  def self.add_select(chain, &block)
    chain << { type: :select, block: block }
  end
  
  # Add sort operation
  def self.add_sort(chain, &block)
    chain << { type: :sort, block: block }
  end
end

# Using iterator combinator
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

operations = IteratorCombinator.operation_chain
IteratorCombinator.add_select(operations) { |n| n.even? }
IteratorCombinator.add_map(operations) { |n| n ** 2 }
IteratorCombinator.add_sort(operations) { |a, b| b <=> a }

result = IteratorCombinator.process(numbers, operations)
puts result.inspect  # [100, 64, 36, 16, 4]

📊 Performance Optimization

Lazy Iterators

ruby
# For large datasets, use lazy iterators
large_range = 1..1_000_000

# Normal way (will process all elements)
# result = large_range.select { |n| n.even? }.map { |n| n ** 2 }.first(10)

# Using lazy for deferred computation
result = large_range.lazy
  .select { |n| n.even? }
  .map { |n| n ** 2 }
  .first(10)

puts result.inspect  # [4, 16, 36, 64, 100, 144, 196, 256, 324, 400]

# Custom lazy iterator
class CustomLazyProcessor
  def initialize(data)
    @data = data
  end
  
  def lazy
    LazyWrapper.new(@data)
  end
end

class LazyWrapper
  def initialize(data)
    @data = data
  end
  
  def map(&block)
    LazyMap.new(self, block)
  end
  
  def select(&block)
    LazySelect.new(self, block)
  end
  
  def take(n)
    LazyTake.new(self, n)
  end
  
  def to_a
    @data.to_a
  end
end

class LazyMap < LazyWrapper
  def initialize(parent, block)
    @parent = parent
    @block = block
  end
  
  def to_a
    @parent.to_a.map(&@block)
  end
end

class LazySelect < LazyWrapper
  def initialize(parent, block)
    @parent = parent
    @block = block
  end
  
  def to_a
    @parent.to_a.select(&@block)
  end
end

class LazyTake < LazyWrapper
  def initialize(parent, n)
    @parent = parent
    @n = n
  end
  
  def to_a
    @parent.to_a.take(@n)
  end
end

Iterator Performance Optimization

ruby
# Avoid unnecessary array creation
# Inefficient way
def inefficient_process(data)
  data.map { |x| x * 2 }
      .select { |x| x > 10 }
      .map { |x| x.to_s }
end

# Efficient way
def efficient_process(data)
  result = []
  data.each do |x|
    doubled = x * 2
    if doubled > 10
      result << doubled.to_s
    end
  end
  result
end

# Using each_with_object for accumulation
def process_with_object(data)
  data.each_with_object([]) do |x, result|
    doubled = x * 2
    result << doubled.to_s if doubled > 10
  end
end

# Batch processing large datasets
def batch_process(data, batch_size = 1000)
  data.each_slice(batch_size) do |batch|
    # Process each batch
    processed_batch = batch.map { |x| x * 2 }
    # Perform other operations...
  end
end

🎯 Iterator Best Practices

1. Choose the Right Iterator Method

ruby
# For simple traversal, use each
[1, 2, 3].each { |n| puts n }

# For transformation, use map
squared = [1, 2, 3].map { |n| n ** 2 }

# For filtering, use select
evens = [1, 2, 3, 4, 5].select(&:even?)

# For checking conditions, use any?/all?
has_even = [1, 2, 3].any?(&:even?)  # true
all_positive = [1, 2, 3].all? { |n| n > 0 }  # true

# For accumulation operations, use reduce
sum = [1, 2, 3, 4, 5].reduce(:+)

# For searching, use find
first_even = [1, 3, 4, 5].find(&:even?)  # 4

2. Block Parameter Best Practices

ruby
# Use meaningful parameter names
users.each { |user| puts user.name }

# For simple single-parameter blocks, use symbol shorthand
names = users.map(&:name)
evens = numbers.select(&:even?)

# For multi-parameter blocks, name them explicitly
users.each_with_index { |user, index| puts "#{index}: #{user.name}" }

# Avoid modifying external variables in blocks
# Not recommended
total = 0
numbers.each { |n| total += n }

# Recommended
total = numbers.reduce(0, :+)
# or
total = numbers.sum

3. Error Handling and Edge Cases

ruby
class SafeIterator
  # Safe iteration processing
  def self.safe_each(collection, &block)
    return [] unless collection.respond_to?(:each)
    
    result = []
    collection.each do |item|
      begin
        result << yield(item) if block_given?
      rescue => e
        puts "Error processing element: #{e.message}"
        result << nil
      end
    end
    result
  end
  
  # Safe mapping
  def self.safe_map(collection, &block)
    return [] unless collection.respond_to?(:map) && block_given?
    
    collection.map do |item|
      begin
        yield(item)
      rescue => e
        puts "Error transforming element: #{e.message}"
        item  # Return original value
      end
    end
  end
  
  # Safe filtering
  def self.safe_select(collection, &block)
    return [] unless collection.respond_to?(:select) && block_given?
    
    collection.select do |item|
      begin
        yield(item)
      rescue => e
        puts "Error filtering element: #{e.message}"
        false  # Don't include error elements by default
      end
    end
  end
end

# Using safe iterators
data = [1, 2, "invalid", 4, 5]
squared = SafeIterator.safe_map(data) { |n| n ** 2 }
puts squared.inspect  # [1, 4, "invalid", 16, 25] (returns original on error)

4. Iterator Usage in Real Applications

ruby
# User data processing
class UserProcessor
  def initialize(users)
    @users = users
  end
  
  # Active users
  def active_users
    @users.select(&:active?)
  end
  
  # Group by age
  def group_by_age_group
    @users.group_by do |user|
      case user.age
      when 0..17 then "minor"
      when 18..35 then "young adult"
      when 36..60 then "middle-aged"
      else "senior"
      end
    end
  end
  
  # Calculate average age
  def average_age
    active_users.map(&:age).reduce(0.0, :+) / active_users.size
  end
  
  # Find VIP users
  def vip_users
    @users.select { |user| user.level >= 5 }
  end
  
  # Username list
  def usernames
    @users.map(&:username).sort
  end
end

# Order processing
class OrderProcessor
  def initialize(orders)
    @orders = orders
  end
  
  # Group orders by status
  def group_by_status
    @orders.group_by(&:status)
  end
  
  # Calculate total revenue
  def total_revenue
    @orders.reduce(0) { |sum, order| sum + order.amount }
  end
  
  # High-value orders
  def high_value_orders(threshold = 1000)
    @orders.select { |order| order.amount > threshold }
  end
  
  # Order statistics by month
  def orders_by_month
    @orders.group_by { |order| order.created_at.strftime("%Y-%m") }
  end
  
  # Recent orders
  def recent_orders(days = 30)
    cutoff_date = Date.today - days
    @orders.select { |order| order.created_at >= cutoff_date }
  end
end

📚 Next Steps

After mastering Ruby iterators, continue learning:

Continue your Ruby learning journey!

Content is for learning and research only.