Skip to Content
Course content

11: Yield and Enumerators

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've always found that the most confusing part of Ruby isn't the syntax—it's the "invisible" passing of blocks. You see each or map everywhere, but if you've ever tried to write your own method that behaves like those, you probably hit a wall pretty quickly. Let's figure out how that actually works by trying to build a simple "batch processor."

The "Magic" Pause Button

Imagine we have a list of heavy tasks, and we want a method that handles the setup and teardown—like opening a connection to a database—and then lets us decide what to do with each item. I'll start with something simple: a method that just yields.

def process_batch(items)
  puts "--- Starting Batch ---"
  items.each do |item|
    yield(item)
  end
  puts "--- Batch Finished ---"
end

process_batch(["email_1", "email_2", "email_3"]) do |email|
  puts "Sending #{email}..."
end

When I run this, it works exactly as expected. The yield keyword is essentially a "pause" button. Ruby jumps out of the process_batch method, runs the code inside the block, and then jumps back right where it left off. It's a powerful way to separate the plumbing (the start/stop logs) from the logic (sending the email).

The Crash when the Block is Missing

But here's the problem. I'm a human, and eventually, I'm going to forget to pass a block. Let's see what happens if I just call the method on its own.

process_batch(["email_1", "email_2"])
# LocalVariableError: undefined method `yield' for ...
# Actually, it's a LocalJumpError: no block given

Ruby doesn't just ignore the yield; it throws a LocalJumpError because it has nowhere to jump to. In a real production app, that's a crash. To fix this, I need to check if a block was even provided. I'll use block_given?.

def process_batch(items)
  puts "--- Starting Batch ---"
  if block_given?
    items.each { |item| yield(item) }
  else
    puts "No block provided, nothing to do!"
  end
  puts "--- Batch Finished ---"
end

That's safer, but it's a bit boring. If I don't provide a block, I just get a message. I'd much rather have the method return something that I can use later. This is where things get interesting.

Turning a Method into a Stream

If you've used Range#each, you know you can do things like (1..10).each.with_index. That .each call is returning an Enumerator. I want my process_batch to do the same. Instead of just printing a warning, I'll use enum_for.

def process_batch(items)
  return enum_for(:process_batch, items) unless block_given?

  puts "--- Starting Batch ---"
  items.each { |item| yield(item) }
  puts "--- Batch Finished ---"
end

# Now, I can do this:
batch = process_batch(["email_1", "email_2", "email_3"])
puts "I have the enumerator, but the batch hasn't started yet!"

# Now I can use any Enumerable method on it:
batch.with_index { |email, idx| puts "#{idx}: #{email}" }

Notice what happened. When I called process_batch without a block, it didn't execute the "Starting Batch" print statement. It just handed me an Enumerator object. The actual execution was deferred until I called .with_index. This is a professional pattern: provide the convenience of a block for simple cases, but return an Enumerator for those who want to chain methods or handle the iteration lazily.

By returning enum_for(:method_name, args), you've essentially told Ruby: "If the user didn't give me a block now, create a package that remembers this method and these arguments, and let the user trigger the yield whenever they're ready."




📋 Practical Task

Build a Log File Line-Filter

You are building a tool to analyze large log files. Instead of loading the whole file into memory, you need to create a method that yields lines one by one. Your goal is to implement a method called filter_logs(filename, keyword).

Requirements:

  • The method should open the file and yield only the lines that contain the specified keyword.
  • If a block is provided, it should execute immediately (yielding the matching lines).
  • If no block is provided, it must return an Enumerator so that the caller can chain methods like .first(5) or .map.
  • Ensure the file is closed properly (use a block for File.open).

Test your code with these two scenarios:

# Scenario 1: Immediate execution
filter_logs("production.log", "ERROR") { |line| puts "Found error: #{line}" }

# Scenario 2: Enumerator chaining
errors = filter_logs("production.log", "ERROR")
puts "The first 3 errors are: #{errors.first(3)}"
Rating
0 0

There are no comments for now.

to be the first to leave a comment.