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. orangeBuilt-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: 3Filter 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
endIterator 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?) # 42. 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.sum3. 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:
- Ruby File I/O - Learn file read/write operations
- Ruby Exception Handling - Master error handling mechanisms
- Ruby Object-Oriented Programming - Deep dive into OOP
- Ruby Blocks and Modules - Learn block and modular programming
Continue your Ruby learning journey!