🚧 Under Construction! Full version coming soon! 🚧

Rails Maze

Yield to Greatness: Writing Smarter Code with Ruby's Enumerable

Yield to Greatness: Writing Smarter Code with Ruby's Enumerable

Published on

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 eaches like an Enumerable 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:

  1. Use .map, .select, .group_by, .reduce, etc.

  2. Apply filters without loading everything

  3. Chain operations lazily using .lazy

  4. 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 Maze Logo Divider

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 or ActiveRecord::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
Rails Maze Logo Divider

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"]
Rails Maze Logo Divider

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.


Continue Learning