In Ruby, when a custom each yields two values, such as yield track, position, what do map, select and each_entry see?
answer
- two yielded values, one block parameter
- map { |x| } sees only the first
- select, to_a and each_entry pack them
- each_entry yields one object per step
- prefer yielding a single object
basics
~20 sEnumerable methods disagree: map { |x| } passes both values, so a one-parameter block gets only the first, while select, to_a and each_entry pack them into [track, position]. each_entry always yields one object per step.
solid answer
~40 sWhen `each` does `yield track, position`, Enumerable methods do not treat the two values the same way. Methods that call the block with the yielded values as they are, such as `map` in CRuby, give a one-parameter block only the **first** value (`map { |x| x }` returns tracks), while two parameters receive both. Methods that pack multiple values into one element, such as `select`, `to_a`, `first` and `each_entry`, see `[track, position]`. `Enumerable#each_entry` exists for exactly this: it calls the block with **one object per iteration**, converting multiple yielded values into an Array and a bare `yield` into `nil`, and returns `self`. The cleaner fix is design: yield one object (a pair or a small `Data` value), as `Hash#each` yields a `[key, value]` pair.
code
ruby · 17 linesclass Playlist
include Enumerable
def initialize(tracks) = (@tracks = tracks)
def each
return enum_for(:each) unless block_given?
@tracks.each_with_index { |track, i| yield track, i + 1 }
self
end
end
list = Playlist.new(%w[Intro Theme])
p list.map { |x| x } # => ["Intro", "Theme"]
p list.select { |x| true } # => [["Intro", 1], ["Theme", 2]]
p list.to_a # => [["Intro", 1], ["Theme", 2]]
list.each_entry { |e| p e } # prints ["Intro", 1] then ["Theme", 2]go deeper
Recall that a custom each should normally yield one object per element, and that each_entry exists for each methods that yield several values.
Explain why map with one parameter sees only the first yielded value while select and to_a see a packed Array, and what each_entry returns.
Spot multi-value yields in adapters and libraries, normalise them with each_entry or a pair, and keep domain collections yielding single objects.
Make single-object yields a rule for shared collection classes so Enumerable behaves uniformly for every consumer.
## The setup A Playlist might want to expose each track together with its position: ```ruby class Playlist include Enumerable def initialize(tracks) = (@tracks = tracks) def each return enum_for(:each) unless block_given? @tracks.each_with_index { |track, i| yield track, i + 1 } self end end ``` `yield track, i + 1` yields **two values**, not one Array. Ruby blocks are lenient about arity: a block with one parameter receives the first value and ignores the rest. Enumerable methods are written in C and each decides how to hand yielded values to *your* block. ## What each method sees In CRuby 4.0's `enum.c`, some methods forward the yielded values unchanged and others pack multiple values into one Array first (the `ENUM_WANT_SVALUE` step): | Call | Block receives | Result | |---|---|---| | `each { \|t, pos\| }` | track, position | as written | | `map { \|x\| x }` | track, position; `x` binds the track | Array of tracks | | `map { \|t, pos\| pos }` | both values | Array of positions | | `select { \|x\| true }` | `[track, position]` | Array of pairs | | `to_a`, `first(2)` | no block | Arrays of `[track, position]` pairs | | `each_entry { \|e\| }` | `[track, position]` | returns the Playlist | So `playlist.map { |x| x }` and `playlist.select { |x| true }` return different shapes from the same data, a subtle source of bugs. ## What each_entry does The rdoc for `Enumerable#each_entry` describes it as calling the block with each element, **converting multiple values from yield to an array**, and returning `self`. Its example: ```ruby class Foo include Enumerable def each yield 1 yield 1, 2 yield end end Foo.new.each_entry { |yielded| p yielded } # 1 # [1, 2] # nil ``` Key points: - One yielded value passes through unchanged. - Several values arrive as one Array. - A bare `yield` arrives as `nil`. - Arguments given to `each_entry` are passed on to `each`. - Without a block it returns an Enumerator. Use it when you consume a third-party `each` that yields several values and you want one object per step regardless. ## The better fix: yield one object If you own the class, avoid the ambiguity: 1. **Yield a pair**: `yield [track, i + 1]`. A two-parameter block still destructures it, and every Enumerable method sees the same element. 2. **Yield a value object**: `Entry = Data.define(:track, :position)` and `yield Entry.new(track, i + 1)`, which reads better at call sites. 3. **Yield just the track** and let callers use `each_with_index` when they need positions. `Hash#each` follows the first approach: it yields one `[key, value]` pair, which is why both `hash.map { |pair| }` and `hash.map { |k, v| }` work consistently. ## How often this comes up Multi-value yields are rare in application code and more common in older libraries and in adapters around C extensions. Knowing `each_entry` is a differentiator; knowing to yield a single object is the design lesson. ## Summary - Yielding several values makes Enumerable methods disagree about what an element is. - `each_entry` normalises that to one object per iteration. - In your own classes, yield one object and the problem disappears.
- Why does hash.map { |pair| pair } return pairs while this Playlist's map { |x| x } returns only tracks?`Hash#each` yields one object, a `[key, value]` Array, so a one-parameter block receives the whole pair. The Playlist yields two separate values, and a one-parameter block binds only the first.
- What does each_entry yield for a bare yield with no arguments?`nil`. The rdoc example shows `yield` arriving as `nil`, a single value arriving unchanged, and several values arriving as one Array.
saying these in an interview costs you the question
- yield track, position is the same as yield [track, position]
- Every Enumerable method sees the same element for a multi-value yield
- each_entry flattens the yielded values into separate iterations
- each_entry returns an Array of the packed entries
- A one-parameter block receives all yielded values as an Array