Ruby is a language built for developer happiness. That phrase might sound fluffy until you realize it's backed by sharp, powerful abstractions that make everyday code feel smooth, expressive, and joyful.
One of the biggest sources of that joy? The Enumerable module.
You've almost certainly used it. If you've ever written .map
, .select
, or .find
, you've tapped into Enumerable. But what you might not realize is just how deep this rabbit hole goes. There's an entire ecosystem of logic, transformation, and laziness hiding behind a single, humble method: #each
.
And the best part? You can wire it up to anything.
Whether you're working with arrays, database rows, log streams, API results, or even raw memory buffers as long as you define #each
, Ruby gives you a whole toolkit of power for free.
Weâre going to go deep. Not just into what Enumerable can do, but why it works and how you can leverage it in ways that feel magical but stay readable, testable, and fast.
By the end, you'll not only understand why Rubyists love Enumerable youâll be able to build your own collections that plug into the entire ecosystem, from .lazy
pipelines to ActiveRecord-style queries.
Letâs start with the core principles that make it all possible.
Enumeration, Base Principles
The Enumerable mixin module is essentially a way to work with collections through powerful block-based syntax. Anyone who has worked with Ruby has seen code like this:
[1, 2, 3].each { |num| p num }
The #each
method is the simplest way to iterate over a collection and is very widely used. So what's going on here? How does an Array
, or any other collection-type object, know about #each
?
Letâs walk through an example that uses a crude ByteMap
so we can see how #each
and Enumerable
work together.
class ByteMap
include Enumerable
SLOT_SIZE = 20
KEY_SIZE = 10
VAL_SIZE = 10
def initialize
@buffer = ""
end
def add(key, value)
key_str = key.to_s.ljust(KEY_SIZE)[0...KEY_SIZE]
val_str = value.to_s.ljust(VAL_SIZE)[0...VAL_SIZE]
@buffer << key_str + val_str
end
def each
i = 0
while i < @buffer.size
raw = @buffer[i, SLOT_SIZE]
key = raw[0, KEY_SIZE].strip
val = raw[KEY_SIZE, VAL_SIZE].strip
yield [key, val]
i += SLOT_SIZE
end
self
end
end
bm = ByteMap.new
bm.add("apple", 1)
bm.add("banana", 2)
bm.each { |k, v| puts "#{k}: #{v}" }
# apple: 1
# banana: 2
bm.map { |k, v| [k.upcase, v.to_i * 10] }
# => [["APPLE", 10], ["BANANA", 20]]
What matters here isnât the storage mechanism: itâs the fact that #each knows how to walk the data and yield each item. Thatâs all Enumerable needs to unlock its full power.
In the code above, essentially what is happening is that we add a key-value pair to the string buffer using the #add
method. After we have some data in memory, we can call #each
and it will yield yield [key, val]
which is very similar to how traditional hash maps are implemented in many languages. They are chunked pieces of memory that are tied to each other through a memory address, or in this situation, position (with the pointer being the index) in the string buffer itself.
Don't be intimidated by the crazy String
manipulation stuff! We only really care about the #each
method and the yield
keyword!
The Enumerable
module doesn't care how your data is stored! All it cares about is that you define an #each
method. That method becomes the gateway to everything else: map
, select
, partition
, and more. Whether you store values in an array, a hash, or a weird string buffer is irrelevant. #each
is the contract. Every time you call an Enumerable method, it uses your #each
to yield
values to the block. That's the entire mechanism behind the magic in a single method powering a rich and expressive API. This is a prime example of how Ruby uses duck typing to create expressive code.
If it walks like an Enumerable and
each
es like anEnumerable
it is one.
Practical Use Cases
This is great and all, but how does knowing about any of this help me? I am already using
map
,each
, and other enumerable methods. They tend to do what I want, so why does this matter?
Simply put: You can establish iterable behavior in any manner that you wish.
Here are some examples of what I mean:
Scoped Audit Logging
class ScopedAuditLog
include Enumerable
def initialize(log_entries)
@entries = log_entries
end
def each
@entries.each do |entry|
next unless [:warn, :error].include?(entry[:severity])
yield entry
end
end
end
# Sample usage:
logs = ScopedAuditLog.new([
{ event: "user_login", severity: :info },
{ event: "failed_payment", severity: :error },
{ event: "password_change", severity: :warn },
{ event: "visit_homepage", severity: :info }
])
error_events = logs.select { |entry| entry[:severity] == :error }
Instead of having to dig through logs like a caveman who just discovered how to CTRL+F, you can establish a way to iterate over log entries and quickly filter them based on custom criteria! In the case above, you know that you don't want any logs unless they are tagged as an error. Even though ScopedAuditLog
isn't an Array
, it acts like one because it implements #each
. Enumerable#select
doesn't care how entries are stored; it just calls #each
and works with what's yielded.
Working With Paginated Results
require 'aws-sdk-dynamodb'
class DynamoTableScanner
include Enumerable
def initialize(table_name:, client: Aws::DynamoDB::Client.new)
@table_name = table_name
@client = client
end
def each
last_evaluated_key = nil
loop do
response = @client.scan(
table_name: @table_name,
exclusive_start_key: last_evaluated_key
)
response.items.each { |item| yield item }
last_evaluated_key = response.last_evaluated_key
break if last_evaluated_key.nil?
end
end
end
scanner = DynamoTableScanner.new(table_name: 'Users')
scanner
.lazy
.map { |item| item['email'] }
.select { |email| email.include?('@company.com') }
.take(10)
If you've worked with AWS API calls, you've probably encountered the classic pagination loop by calling .scan
, tracking the LastEvaluatedKey
, repeating until done. It's verbose and often tied tightly to one table or structure.
With Enumerable, we abstract that boilerplate into a single #each
method and now we can:
Use
.map
,.select
,.group_by
,.reduce
, etc.Apply filters without loading everything
Chain operations lazily using
.lazy
Keep the code table-agnostic and reusable
The Enumerable contract transforms your AWS pagination loop into a composable, expressive Ruby pipeline. Very cool!
Schema Validations
class SchemaValidator
include Enumerable
def initialize(model)
@model = model
end
def each
yield [:email, "is missing"] if @model.email.nil?
yield [:age, "must be over 18"] if @model.age && @model.age < 18
# Add more checks
end
end
# Usage
errors = SchemaValidator.new(user)
errors.to_h
# => {:email=>"is missing", :age=>"must be over 18"}
You can compose validations as a flexible result stream instead of relying on statically-checked values. This approach decouples your validation logic from any specific model or schema, making it completely context-agnostic. The real beauty is that errors become just another enumerable collection. This means that you can filter, map, group, or transform them like any other dataset.
Best of all, you're not tied to ActiveRecord or its callbacks. This makes your validation logic reusable across plain Ruby objects which is perfect for working with CSV rows, DTOs, or external API payloads where no database model exists. It's validation as data, not as framework magic.
These examples all share one idea: define how to iterate, and Ruby will do the rest. By writing a smart #each
, you're not just building a collection: you're building an interface to power transformation, filtering, streaming, and reporting. That's what makes Enumerable one of Ruby's quiet superpowers.
Rails Enumerable Utilities
Rails has some fantastic utilities to make working with Enumerable objects feel seamless and powerful, but many of these features are underutilized. In this section, we'll explore how Rails and core Ruby features like enum_for
let you write more expressive, flexible, and maintainable code with very little overhead.
Imagine you're working with a custom collection. This could be a filtered audit log, a stream of records from an API, or a background job queue. You want to use powerful Enumerable methods like .map
, .select
, .with_index
, or .lazy
. But your collection doesn't behave like an Array
and calling each without a block raises an error or does nothing.
Now you're stuck. You either:
- Always have to pass a block to
#each
- Or can't chain methods the way you would with
Array
orActiveRecord::Relation
Enhanced ScopedAuditLog
with enum_for
class ScopedAuditLog
include Enumerable
def initialize(log_entries)
@entries = log_entries
end
def each
return enum_for(:each) unless block_given?
@entries.each do |entry|
next unless [:warn, :error].include?(entry[:severity])
yield entry
end
end
end
# Usage without a block
log_data = [
{ event: "user_login", severity: :info },
{ event: "failed_payment", severity: :error },
{ event: "password_change", severity: :warn }
]
logs = ScopedAuditLog.new(log_data)
# No block? You get an Enumerator!
logs.each.with_index do |entry, i|
puts "#{i + 1}. #{entry[:event]}"
end
By using enum_for
(aka to_enum
), you can make your custom classes behave like native Ruby collections. This small addition makes your each method chainable, composable, and lazy-capable just like the best parts of Rails and Ruby.
The enum_for
method allows you to use your #each
method without providing a block. Instead, it returns an Enumerator object, enabling you to chain methods like map
, with_index
, or lazy
. This works just like with any other enumerable!
Using #pluck
for Efficient Data Extraction
In Rails, ActiveRecord::Relation
objects behave like Enumerables, but Rails enhances them with additional methods for performance and convenience. One of the most useful is #pluck
.
pluck
allows you to retrieve specific columns directly from the database without instantiating full model objects. This reduces memory usage and improves performance, particularly when you only need a few fields.
Let's say you are trying to retrieve all of the emails in your app:
emails = User.all.map(&:email)
This works, but is wildly inefficient. It loads entire User objects into memory by pulling in all columns even if you only need the email.
Instead, we can #pluck
the necessary column valued without having to get the full record:
User.pluck(:email)
# => ["user1@example.com", "user2@example.com", ...]
This generates a SQL query that fetches only the email column, and returns a plain Ruby array instead of ActiveRecord objects. Simple, neat, and far more efficient usage of the tools at your disposal!
You can also pass more than one argument to #pluck
to pull multiple columns into a nested array structure so you can build a hash, populate dropdown lists, or do bulk lookups with the minimum necessary data:
User.pluck(:id, :email)
# => [[1, "user1@example.com"], [2, "user2@example.com"]]
TL;DR:
#map(&:field)
pulls full model instances which is much slower and heavier#pluck(:field)
pulls raw data which is faster and lighter- Use pluck when you want data, not objects
Bonus: Cool & Useful Enumerable Methods in Ruby & Rails
group_by
Organize items into buckets based on a block.
users.group_by { |u| u.role }
# => { "admin" => [user1, user2], "member" => [user3] }
tally
Count occurrences of values in a collection.
%w[dog cat dog bird].tally
# => { "dog" => 2, "cat" => 1, "bird" => 1 }
find
or detect
Return the first element matching a condition. It's usually best to use detect
in a Rails context since ActiveRecord defines a find
method and it keeps the operations distinct.
users.find { |u| u.email_verified? } # Works in plain Ruby, but less clear in Rails
users.detect { |u| u.email_verified? } # Preferred in Rails for clarity and safety
partition
Split a collection into two groups based on a predicate.
users.partition { |u| u.admin? }
# => [[admin_users], [non_admin_users]]
chunk
Split a collection into slices based on return value of a block.
%w[cat cat dog dog dog cat].chunk(&:itself).to_a
# => [["cat", ["cat", "cat"]], ["dog", ["dog", "dog", "dog"]], ["cat", ["cat"]]]
cycle
Repeat the collection forever (or n times).
[1, 2].cycle(3).to_a
# => [1, 2, 1, 2, 1, 2]
zip
A personal favorite! Combine two or more collections index-wise.
names = ["Shrek", "Donkey"]
scores = [90, 50]
names.zip(scores)
# => [["Shrek", 90], ["Donkey", 50]]
grep
and grep_v
Think of grep
as a smart filter that finds items matching a pattern or class.
words = ["apple pie", "banana", "apple crisp", "orange"]
# Get only items that include "apple"
words.grep(/apple/)
# => ["apple pie", "apple crisp"]
And the opposite with grep_v
. It's essentially reject
instead of select
.
words = ["apple pie", "banana", "apple crisp", "orange"]
words.grep_v(/apple/)
# => ["banana", "orange"]
Wrapping Up
Ruby's Enumerable module is one of its most elegant and empowering features, and as we've seen, it goes far beyond just calling .map
or .each
.
By understanding how #each
powers the entire system, you unlock the ability to:
Write your own lazily iterable classes
Build expressive pipelines for filtering and transformation
Integrate with external data sources like DynamoDB or logs
Validate, scan, or manipulate data using nothing but pure Ruby
Rails adds even more to the mix, with tools like pluck
, index_by
, and ActiveRecord scopes that act like Enumerables but hit the database with precision.
And let's not forget the unsung heroes: methods like partition
, grep
, and each_with_object
, which turn complex data logic into readable, composable code.
Let's be honest: how many other languages let you build a DynamoDB paginator, a CSV validator, and a custom log filter all using one method?
It's just #each
. That's the whole trick. You define how to yield, and Ruby does the rest.
If there's one takeaway, it's this:
Enumerable isn't just a module. It's a mindset.
Build your data to yield, and Ruby will do the rest.
Whether you're optimizing for performance, writing custom logic, or just trying to make your code feel more âRubyish,â tapping into Enumerable gives you an edge and produces code that is a joy to work with.