In a Ruby class that includes Enumerable, what should each return with and without a block, and why does it matter?
answer
- no block: an Enumerator, not LocalJumpError
- with a block: self
- never hand out the internal Array
- define size for Enumerator sizes
- external next and with_index need it
basics
~10 sWithout 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 sEnumerable'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 linesTrack = 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 Arraygo deeper
Recall the guard line return enum_for(:each) unless block_given? and that each with a block usually returns self.
Explain which calls break without the guard, why returning self matters, and how a size block lets the Enumerator report its size.
Catch the delegation shortcut that leaks internal storage, and design each to be side-effect free and safe against mutation during iteration.
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