Loading
All articles
Programming 4 min read

Ruby Blocks and Iterators: Write Your Own

Learn how Ruby methods receive blocks, pass values with yield, handle a missing block, and return an Enumerator. Build and run your own iterator.

nbkhope
On this page

Ruby Blocks and Iterators: Write Your Own

You have probably written numbers.each { |number| puts number }. The interesting question is what happens on the method's side of that call. How does each hand a number to your block? What happens when nobody supplies a block?

This guide builds a small iterator so you can see both sides. If each, map, and select are still new to you, start with the Ruby arrays guide. The examples below were run with Ruby 3.3.

A Ruby method passes values one at a time to a caller's block, then returns an Enumerator when no block is supplied.

The method decides when the block runs

A block is code attached to a method call. Its parameters, between | characters, receive values from the method. Inside a method, yield calls that block:

1def twice
2  yield 1
3  yield 2
4end
5
6twice do |number|
7  puts "Visit #{number}"
8end
Visit 1
Visit 2

The caller provides the block; twice controls when it runs and which values it receives. The do ... end form is handy for several lines. Braces work for a short call: twice { |number| puts number }.

The value of a yield expression is the block's return value. A method can use it, as this tiny transformer does:

1def transform_two
2  [yield(1), yield(2)]
3end
4
5p transform_two { |number| number * 10 }
[10, 20]

This is the same idea behind transformations such as map: the method gathers values returned by the block. In contrast, an iterator can simply hand values over and ignore the block's return values.

Build an iterator for passing scores

Suppose a report should visit only scores at or above a chosen minimum. We can write each_passing to decide which scores reach the caller's block:

1def each_passing(scores, minimum)
2  return enum_for(__method__, scores, minimum) unless block_given?
3
4  scores.each do |score|
5    yield score if score >= minimum
6  end
7end
8
9scores = [48, 82, 67, 91]
10each_passing(scores, 70) { |score| puts "Passed: #{score}" }
Passed: 82
Passed: 91

Read the method from top to bottom:

  1. block_given? asks whether this call includes a block.
  2. With a block, scores.each walks the input in order. The condition decides whether to yield a score.
  3. The caller's block prints each score it receives. The method does not collect its return values. With a block, this implementation returns scores, because scores.each returns its receiver.
Ruby control-flow diagram: the caller invokes each_passing, the method tests a score and yields 82 to the caller's block, then execution returns to the method. Without a block, the method returns an Enumerator.

The arrow back matters: after the block runs for 82, each_passing resumes its loop and can yield 91. The block does not take over the whole iteration; the method remains in charge of which values arrive and when.

The minimum is an ordinary method argument. The block is additional behavior supplied by the caller. Neither the method nor the input array needs to know whether the caller will print, count, or save the scores.

No block? Return something useful

Without the first line of each_passing, reaching yield with no block would raise LocalJumpError. Returning an Enumerator instead lets the caller decide how to consume the values later:

1passing = each_passing([48, 82, 67, 91], 70)
2
3p passing.class
4p passing.to_a
5p passing.first
Enumerator
[82, 91]
82

enum_for(__method__, scores, minimum) creates an enumerator for this method and remembers its arguments. When to_a or first asks for values, the enumerator calls each_passing with a block. It does not create the filtered array at the point where passing is assigned. Each consumption runs the method again, so changes to the original input can affect later results.

This two-way interface—yield to a supplied block, or return an enumerator—is common in Ruby's collection methods. If your method requires a block, you can omit the guard and document that requirement instead. Choose the behavior deliberately.

Try it yourself

Write each_long_word(words, minimum_length). It should yield words whose length is at least the minimum and return an enumerator if called without a block. Try each_long_word(%w[cat otter elephant], 5).to_a before looking at the answer.

<details> <summary>One solution</summary>
1def each_long_word(words, minimum_length)
2  return enum_for(__method__, words, minimum_length) unless block_given?
3
4  words.each do |word|
5    yield word if word.length >= minimum_length
6  end
7end
8
9p each_long_word(%w[cat otter elephant], 5).to_a
["otter", "elephant"]
</details>

The key idea is simple: a block belongs to the caller, while the method chooses when to call it. Once you can write that method, built-in iterators feel less mysterious.

Further reading

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.