Table of Contents
- Introduction to Enumerable
- How Enumerable Works: The
eachMethod - Core Enumerable Methods You Should Know
- Lesser-Known Gems: Hidden Enumerable Methods
- Advanced Tips and Tricks
- Practical Examples: Real-World Use Cases
- References and Further Reading
Introduction to Enumerable
At its core, Enumerable is a Ruby module that provides over 50 methods for working with collections. It’s included in built-in classes like Array, Hash, Range, Set, and Enumerator, making it ubiquitous in Ruby code.
The magic of Enumerable lies in its ability to abstract iteration logic. Instead of writing repetitive loops (e.g., for or while), you use declarative methods like map, select, or inject to express what you want to do, not how to do it. This makes Ruby code concise, readable, and less error-prone.
How Enumerable Works: The each Method
Before you can use Enumerable, a class must implement a single method: each. The each method defines how elements are iterated over (e.g., one by one for an array, key-value pairs for a hash) by yielding elements to a block.
Enumerable then uses this each method to power all its other methods. For example, map calls each under the hood, applies the block to each yielded element, and collects the results.
Example: A Custom Enumerable Class
To see this in action, let’s create a simple Playlist class that includes Enumerable:
class Playlist
def initialize(songs)
@songs = songs # @songs is an array of song titles
end
# Implement `each` to yield each song
def each
@songs.each { |song| yield song }
end
include Enumerable # Now we get all Enumerable methods!
end
# Usage
my_playlist = Playlist.new(["Bohemian Rhapsody", "Hey Jude", "Hotel California"])
# Enumerable methods now work on my_playlist:
my_playlist.select { |song| song.length > 10 } # => ["Bohemian Rhapsody", "Hotel California"]
my_playlist.map { |song| song.upcase } # => ["BOHEMIAN RHAPSODY", "HEY JUDE", "HOTEL CALIFORNIA"]
By defining each and including Enumerable, Playlist gains access to every method in Enumerable—no extra code needed!
Core Enumerable Methods You Should Know
Let’s explore the workhorses of Enumerable—methods you’ll use daily.
map/collect: Transform Elements
map (aliased as collect) applies a block to each element and returns a new array of the results. Use it to transform data.
Syntax: enumerable.map { |element| ... }
Example:
numbers = [1, 2, 3, 4]
squared = numbers.map { |n| n **2 } # => [1, 4, 9, 16]
# With strings
words = ["hello", "world"]
capitalized = words.collect { |word| word.capitalize } # => ["Hello", "World"]
select/find_all: Filter Elements
select (aliased as find_all) returns an array of elements that match a condition (i.e., the block returns true).
Syntax: enumerable.select { |element| condition }
Example:
numbers = (1..10).to_a
evens = numbers.select { |n| n.even? } # => [2, 4, 6, 8, 10]
# Filter users by age
users = [{ name: "Alice", age: 25 }, { name: "Bob", age: 17 }]
adults = users.select { |user| user[:age] >= 18 } # => [{ name: "Alice", age: 25 }]
reject: Inverse of Select
reject returns elements that do not match the condition (the opposite of select).
Syntax: enumerable.reject { |element| condition }
Example:
numbers = (1..10).to_a
odds = numbers.reject { |n| n.even? } # => [1, 3, 5, 7, 9]
# Reject empty strings
words = ["apple", "", "banana", nil, "cherry"]
non_empty = words.reject { |word| word.to_s.empty? } # => ["apple", "banana", "cherry"]
find/detect: Find the First Match
find (aliased as detect) returns the first element that matches the condition. It stops iterating once a match is found (efficient for large collections).
Syntax: enumerable.find { |element| condition }
Example:
users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" }
]
# Find the first admin
first_admin = users.find { |user| user[:role] == "admin" } # => { name: "Alice", role: "admin" }
inject/reduce: Accumulate Values
inject (aliased as reduce) is used to accumulate a value from elements (e.g., sum, product, concatenation). It takes an optional initial value and a block with an accumulator and current element.
Syntax:
- Without initial value:
enumerable.inject { |acc, element| ... }(uses first element as initialacc) - With initial value:
enumerable.inject(initial) { |acc, element| ... }
Examples:
# Sum numbers (without initial value)
(1..5).inject { |sum, n| sum + n } # => 15 (1+2+3+4+5)
# Sum numbers (with initial value 0)
(1..5).inject(0) { |sum, n| sum + n } # => 15 (safer if collection might be empty)
# Concatenate strings
words = ["Hello", " ", "World", "!"]
words.inject("") { |sentence, word| sentence + word } # => "Hello World!"
# Calculate product
(2..4).inject(1) { |product, n| product * n } # => 24 (1*2*3*4)
all?/any?/none?/one?: Boolean Checks
These methods return a boolean based on whether elements match a condition:
all?:trueif all elements match.any?:trueif at least one element matches.none?:trueif no elements match.one?:trueif exactly one element matches.
Examples:
numbers = [2, 4, 6, 8]
numbers.all? { |n| n.even? } # => true (all even)
numbers.any? { |n| n > 10 } # => false (none >10)
numbers.none? { |n| n.odd? } # => true (no odds)
numbers.one? { |n| n == 6 } # => true (exactly one 6)
Omit the block to check if elements are “truthy” (e.g., non-nil, non-false):
[1, 2, nil, 3].any? # => true (has truthy elements)
[nil, false].all? # => false (not all truthy)
sort_by: Custom Sorting
sort_by sorts elements based on a transformed value (e.g., length, a hash key). It’s often more efficient than sort because it computes the transformed value once per element.
Syntax: enumerable.sort_by { |element| ... }
Example:
# Sort strings by length (shortest to longest)
words = ["apple", "banana", "cherry", "date"]
words.sort_by { |word| word.length } # => ["date", "apple", "cherry", "banana"]
# Sort users by age (youngest to oldest)
users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
users.sort_by { |user| user[:age] } # => [{ name: "Bob", ... }, { name: "Alice", ... }, ...]
Lesser-Known Gems: Hidden Enumerable Methods
Beyond the core methods, Enumerable has several underrated tools for specific tasks.
grep: Pattern Matching
grep selects elements that match a pattern using the === operator (e.g., regex, class, range). It’s like a specialized select for pattern matching.
Syntax: enumerable.grep(pattern)
Examples:
# Match regex pattern (find strings with "cat")
words = ["cat", "category", "dog", "catastrophe"]
words.grep(/cat/) # => ["cat", "category", "catastrophe"]
# Match class (find integers)
mixed = [1, "two", 3.0, 4, "five"]
mixed.grep(Integer) # => [1, 4] (3.0 is Float, not Integer)
# Match range (find numbers between 5-10)
(1..15).grep(5..10) # => [5, 6, 7, 8, 9, 10]
flat_map: Map and Flatten
flat_map is equivalent to map followed by flatten(1) (it flattens nested arrays one level). Use it to process nested collections.
Syntax: enumerable.flat_map { |element| ... }
Example:
# List all tags from blog posts (posts have arrays of tags)
posts = [
{ title: "Post 1", tags: ["ruby", "enumerable"] },
{ title: "Post 2", tags: ["rails", "ruby"] }
]
all_tags = posts.flat_map { |post| post[:tags] } # => ["ruby", "enumerable", "rails", "ruby"]
unique_tags = all_tags.uniq # => ["ruby", "enumerable", "rails"]
group_by: Group Elements by Key
group_by groups elements into a hash, where keys are the result of the block, and values are arrays of elements with that key.
Syntax: enumerable.group_by { |element| key }
Example:
# Group users by role
users = [
{ name: "Alice", role: "admin" },
{ name: "Bob", role: "user" },
{ name: "Charlie", role: "admin" },
{ name: "Diana", role: "user" }
]
users_by_role = users.group_by { |user| user[:role] }
# Result:
# {
# "admin" => [{ name: "Alice", ... }, { name: "Charlie", ... }],
# "user" => [{ name: "Bob", ... }, { name: "Diana", ... }]
# }
partition: Split into Two Arrays
partition splits elements into two arrays: those that match the condition, and those that don’t (like select and reject combined).
Syntax: enumerable.partition { |element| condition }
Example:
numbers = (1..10).to_a
evens, odds = numbers.partition { |n| n.even? }
evens # => [2, 4, 6, 8, 10]
odds # => [1, 3, 5, 7, 9]
# Split users into admins and non-admins
users = [
{ name: "Alice", admin: true },
{ name: "Bob", admin: false },
{ name: "Charlie", admin: true }
]
admins, non_admins = users.partition { |user| user[:admin] }
min_by/max_by: Find Min/Max by Criteria
min_by and max_by find the element with the minimum or maximum value based on a block result (like sort_by but for single min/max).
Examples:
# Find shortest word
words = ["apple", "banana", "cherry", "date"]
words.min_by { |word| word.length } # => "date" (length 4)
# Find oldest user
users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
]
users.max_by { |user| user[:age] } # => { name: "Charlie", age: 35 }
take/drop: Limit or Skip Elements
take(n): Returns the firstnelements.drop(n): Returns all elements after skipping the firstn.
Examples:
(1..10).take(3) # => [1, 2, 3] (first 3 elements)
(1..10).drop(7) # => [8, 9, 10] (skip first 7, take remaining)
# Get top 2 scores
scores = [95, 88, 92, 78, 90]
top_scores = scores.sort.reverse.take(2) # => [95, 92]
cycle: Repeat Indefinitely
cycle repeats the collection indefinitely. Use take(n) to limit iterations (avoid infinite loops!).
Example:
# Repeat [1, 2, 3] 5 times
[1, 2, 3].cycle.take(5) # => [1, 2, 3, 1, 2]
# Create a repeating sequence for a slideshow
slides = ["Intro", "Features", "Q&A"]
slideshow = slides.cycle.take(10) # Repeats slides 10 times
Advanced Tips and Tricks
Chaining Enumerable Methods
Enumerable methods return enumerables, so you can chain them to build powerful data pipelines.
Example: Process and analyze order data:
orders = [
{ id: 1, total: 99.99, status: "completed" },
{ id: 2, total: 49.99, status: "pending" },
{ id: 3, total: 149.99, status: "completed" },
{ id: 4, total: 29.99, status: "completed" },
{ id: 5, total: 79.99, status: "refunded" }
]
# 1. Filter completed orders
# 2. Extract totals
# 3. Sum the totals
total_revenue = orders
.select { |order| order[:status] == "completed" }
.map { |order| order[:total] }
.inject(0, :+) # Shorthand for { |sum, total| sum + total }
total_revenue # => 99.99 + 149.99 + 29.99 = 279.97
Symbols as Procs: Shorter Syntax
When the block is a simple method call (e.g., upcase, length), use &:method_name instead of a full block. This is a shorthand for { |element| element.method_name }.
Examples:
["apple", "banana"].map(&:upcase) # => ["APPLE", "BANANA"] (same as { |s| s.upcase })
[1, 2, 3, 4].select(&:even?) # => [2, 4] (same as { |n| n.even? })
users.map(&:age) # => [30, 25, 35] (extracts :age from each user hash)
Custom Enumerable Classes
As we saw earlier, any class that implements each can include Enumerable. This is useful for wrapping custom collections (e.g., a TodoList or Inventory).
Example: A TodoList Class
class TodoList
def initialize
@todos = []
end
def add(todo)
@todos << todo
end
# Implement `each` to yield todos
def each(&block)
@todos.each(&block) # Delegate to array's each
end
include Enumerable # Now we get all Enumerable methods!
end
# Usage
todos = TodoList.new
todos.add("Buy milk")
todos.add("Write blog")
todos.add("Call mom")
todos.select { |todo| todo.include?("blog") } # => ["Write blog"]
todos.any? { |todo| todo.empty? } # => false (no empty todos)
Lazy Enumerators for Large Data
For large or infinite collections, use lazy to defer computation until needed. This avoids loading all elements into memory at once.
Example: Process a large log file
# Without lazy: Reads entire file into memory first
large_file = File.foreach("huge_log.txt").select { |line| line.include?("ERROR") }
# With lazy: Processes line-by-line, stops early if needed
lazy_errors = File.foreach("huge_log.txt").lazy.select { |line| line.include?("ERROR") }
first_5_errors = lazy_errors.take(5).to_a # Only loads first 5 error lines
Example: Infinite sequence
# Generate first 10 even numbers (without iterating infinitely)
even_numbers = (1..Float::INFINITY).lazy.select(&:even?)
even_numbers.take(10).to_a # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
Practical Examples: Real-World Use Cases
Example 1: Processing E-Commerce Orders
Let’s use Enumerable to analyze a month’s worth of orders:
orders = [
{ id: 1, customer: "Alice", total: 99.99, items: 2 },
{ id: 2, customer: "Bob", total: 49.99, items: 1 },
{ id: 3, customer: "Alice", total: 149.99, items: 3 },
{ id: 4, customer: "Charlie", total: 29.99, items: 1 },
{ id: 5, customer: "Bob", total: 79.99, items: 2 }
]
# 1. Group orders by customer
orders_by_customer = orders.group_by { |o| o[:customer] }
# => { "Alice"=>[...], "Bob"=>[...], "Charlie"=>[...] }
# 2. Calculate total spent per customer
customer_totals = orders_by_customer.transform_values do |customer_orders|
customer_orders.sum { |o| o[:total] }
end
# => { "Alice"=>249.98, "Bob"=>129.98, "Charlie"=>29.99 }
# 3. Find customers who bought >2 items in a single order
big_spenders = orders_by_customer.select do |_name, orders|
orders.any? { |o| o[:items] > 2 }
end.keys
# => ["Alice"] (Alice has an order with 3 items)
Example 2: Analyzing User Data
Let’s clean and analyze user data from a CSV:
# Sample user data (simulated CSV input)
users = [
{ name: "Alice", age: 28, active: true, signup_date: "2023-01-15" },
{ name: "Bob", age: 35, active: false, signup_date: "2022-11-30" },
{ name: "Charlie", age: 22, active: true, signup_date: "2023-03-20" },
{ name: "Diana", age: 40, active: true, signup_date: "2021-05-05" }
]
# 1. Filter active users aged 25-35
target_users = users.select do |user|
user[:active] && user[:age].between?(25, 35)
end
# => [{ name: "Alice", ... }, { name: "Bob", ... }] (Bob is inactive, so only Alice)
# 2. Sort by signup date (newest first)
sorted_users = target_users.sort_by { |user| Date.parse(user[:signup_date]) }.reverse
# => [{ name: "Alice", ... }] (only Alice, sorted newest first)
# 3. Extract names and ages as a hash
user_info = sorted_users.map { |u| [u[:name], u[:age]] }.to_h
# => { "Alice"=>28 }
References and Further Reading
- Ruby Official Documentation: Enumerable
- RubyGuides: Enumerable Methods
- Pragmatic Programmers: “Programming Ruby” (The Pickaxe Book)
- Ruby Monk: Enumerable Tutorial
By mastering Enumerable, you’ll write cleaner, more expressive Ruby code. Experiment with these methods, chain them creatively, and don’t hesitate to dive into the Ruby docs for even more hidden features! Happy coding! 🚀