skip to content

In a Ruby class that includes Enumerable, what should each return with and without a block, and why does it matter?

level: seniorimportance: should knowfreq 30%

answer

  1. no block: an Enumerator, not LocalJumpError
  2. with a block: self
  3. never hand out the internal Array
  4. define size for Enumerator sizes
  5. external next and with_index need it

basics

~10 s

Without a block, each should return an Enumerator (return enum_for(:each) unless block_given?); with a block it should return self, like Array#each. That keeps next, each.with_index and chaining working and avoids leaking the internal Array.

solid answer

~40 s

Enumerable's own methods call `each` with a block, so they work even if `each` has no blockless form; but callers also call `each` directly. Without a block, a `yield`-based `each` raises `LocalJumpError`, which breaks `playlist.each.with_index(1)`, `playlist.each.next` and passing `playlist.each` around; the guard `return enum_for(:each) { size } unless block_given?` fixes that, and the size block lets the Enumerator report a size. With a block, return `self` so the call chains like `Array#each`. The shortcut `def each(&block) = @tracks.each(&block)` works without a block, but with a block it returns `@tracks`, handing callers the internal Array to mutate. Defining `size` also lets Enumerable's own Enumerators report sizes, because they ask the receiver for `size`.

code

ruby · 21 lines
ruby
Track = Data.define(:title, :seconds)

class Playlist
  include Enumerable

  def initialize(tracks) = (@tracks = tracks.dup)
  def size = @tracks.size

  def each
    return enum_for(:each) { size } unless block_given?

    @tracks.each { |track| yield track }
    self
  end
end

list = Playlist.new([Track.new("Intro", 95), Track.new("Theme", 240), Track.new("Coda", 130)])
list.each.with_index(1) { |t, n| puts "#{n}. #{t.title}" }
p list.each.size            # => 3
p list.each_slice(2).size   # => 2, via Playlist#size
p list.each { }.equal?(list) # => true, not the internal Array

go deeper

for a junior

Recall the guard line return enum_for(:each) unless block_given? and that each with a block usually returns self.

for a middle

Explain which calls break without the guard, why returning self matters, and how a size block lets the Enumerator report its size.

for a senior

Catch the delegation shortcut that leaks internal storage, and design each to be side-effect free and safe against mutation during iteration.

for a principal

Treat each as a public contract for every domain collection and set a house pattern so all of them behave like core collections.

## Two callers of each A Playlist's `each` is called in two very different ways: 1. **By Enumerable itself.** `map`, `select`, `sort_by` and the rest call `each` **with** a block. 2. **By application code.** Callers write `playlist.each { ... }`, but also `playlist.each.with_index(1)`, `playlist.each.next`, or pass `playlist.each` to another method, all **without** a block. A minimal `each` that only yields satisfies the first caller and fails the second: ```ruby def each @tracks.each { |t| yield t } end playlist.each.with_index(1) { |t, n| puts "#{n}. #{t.title}" } # LocalJumpError: no block given (yield) ``` ## The robust shape ```ruby def each return enum_for(:each) { size } unless block_given? @tracks.each { |track| yield track } self end def size = @tracks.size ``` | Situation | Returns | Why | |---|---|---| | no block | an `Enumerator` over the Playlist | chaining, external `next`, lazy consumers | | with a block | `self` | matches `Array#each`; allows `playlist.each { ... }.map { ... }` | | size block given | Enumerator knows its `size` | `playlist.each.size` answers without iterating | ## The delegation shortcut and its leak A popular one-liner delegates straight to the internal Array: ```ruby def each(&block) = @tracks.each(&block) ``` - Without a block, `@tracks.each` returns an Enumerator over the internal Array. That works for iteration. - With a block, `Array#each` returns **its receiver**, the internal `@tracks`. A caller can now write `playlist.each { }.clear` or `<< track` and bypass every rule the Playlist enforces (a maximum length, no duplicates, validation). Fixing it takes one line: call the block and return `self`. ```ruby def each(&block) return enum_for(:each) { size } unless block @tracks.each(&block) self end ``` ## Why a size method helps Enumerable too Enumerable builds its own blockless Enumerators, for example `playlist.each_slice(10)` or `playlist.map`. To give those a `size`, CRuby's `enum_size` in `enum.c` calls the receiver's `size` method if it exists and returns `nil` otherwise. So: - with `Playlist#size`, `playlist.each_slice(10).size` answers `(size / 10.0).ceil` without iterating; - without it, the same call returns `nil`. `size` should be cheap and exact; if the class can only count by iterating, leave it out and let callers use `count`. ## Checklist for a production-quality each - Guard the blockless call with `enum_for(:each)`, adding a size block when the size is cheap. - Yield each element exactly once, in a defined order. - Return `self`, never the internal storage. - Do not mutate the collection while yielding; iterate over a snapshot (`@tracks.dup.each`) if the block might change the Playlist. - Keep `each` free of side effects such as network calls, or document them, since every Enumerable method calls it again. ## Testing the contract A short spec pins the behaviour so a later refactor cannot quietly break it: 1. `playlist.each` without a block returns an `Enumerator`, and `playlist.each.to_a` equals the tracks in order. 2. `playlist.each { }` returns the Playlist itself (`equal?`), not an Array. 3. `playlist.each.size` equals `playlist.size` without iterating, if a size block is given. 4. The object returned by `each { }` does not respond to `clear` or `<<`, so callers cannot reach the internal Array. These four checks catch the missing guard, the leaked internal Array and a stale size block, the three defects this question is about. ## Summary `each` is the one method Enumerable needs, but callers also treat it as a public API. Return an Enumerator without a block, `self` with one, and define `size` if you know it.

  • Do Enumerable's own methods such as map break if each has no blockless form?
    No. They always call `each` with a block, and their blockless forms build Enumerators that later call them with a block. What breaks is direct use of `playlist.each` without a block, such as `each.with_index` or `each.next`.
  • Why iterate over a snapshot inside each?
    If the block adds or removes tracks while `each` is walking `@tracks`, the iteration can skip or repeat elements. Iterating `@tracks.dup` gives the block a stable view at the cost of one shallow copy.

saying these in an interview costs you the question

  • Enumerable's map fails unless each returns an Enumerator without a block
  • Returning @tracks.each(&block) from each is harmless
  • each with a block should return the block's last value
  • Enumerable computes Enumerator sizes by iterating each
  • Without a block, a yield-based each simply returns nil