In Ruby, how do you make your own iterator method return an Enumerator when called without a block, using to_enum or enum_for?
answer
- guard with block_given?
- enum_for(__method__, *args)
- to_enum and enum_for are one method
- size block answers without iterating
- LocalJumpError without the guard
basics
~20 sStart the method with return enum_for(method, args) unless block_given?. The Enumerator re-calls the method with the same arguments when iterated; an optional block to enum_for computes size. Without the guard, yield with no block raises LocalJumpError.
solid answer
~40 s`Kernel#to_enum` and `Kernel#enum_for` are the same method: `obj.enum_for(:meth, *args)` returns an Enumerator that calls `obj.meth(*args)` whenever it is iterated. The idiom for your own iterator is `return enum_for(__method__, symbol) unless block_given?` at the top of the method, so a blockless call gets an Enumerator and every Enumerable chain works on it. `__method__` keeps the name right if the method is renamed, and passing the arguments through is essential, otherwise the Enumerator calls the method without them. A block given to `enum_for` computes `size` lazily (`enum_for(__method__, n) { n }`); without it, `size` returns `nil`. Without the guard, the first `yield` raises `LocalJumpError` (`no block given (yield)`).
code
ruby · 18 linesTick = Data.define(:symbol, :price)
class TickLog
def initialize(ticks)
@ticks = ticks
end
def each_tick(symbol)
return enum_for(__method__, symbol) unless block_given?
@ticks.each { |t| yield t if t.symbol == symbol }
self
end
end
log = TickLog.new([Tick.new("ACME", 101), Tick.new("INIT", 99), Tick.new("ACME", 103)])
p log.each_tick("ACME").map { |t| t.price } # => [101, 103]
p log.each_tick("ACME").size # => nil, no size blockgo deeper
Recall the one-line guard: return enum_for(method) unless block_given?, placed at the top of a method that yields.
Explain why the arguments must be passed through, what the optional size block does, and that to_enum and enum_for are the same method.
Watch for re-executed I/O when an Enumerator is iterated repeatedly, and keep size blocks exact or omit them.
Standardise iterator APIs on the enum_for guard so every collection-like object composes with Enumerable, lazy chains and external iteration.
## The problem A method that yields is an **internal iterator**: the caller hands it a block. If a caller forgets the block, `yield` has nothing to call: ```ruby def each_tick(symbol) @ticks.each { |t| yield t if t.symbol == symbol } end feed.each_tick("ACME") # LocalJumpError: no block given (yield) ``` Core iterators avoid this by returning an `Enumerator` when there is no block. Your methods can do the same with `to_enum` or `enum_for`. ## The idiom ```ruby def each_tick(symbol) return enum_for(__method__, symbol) unless block_given? @ticks.each { |t| yield t if t.symbol == symbol } end ``` What each piece does: 1. **`block_given?`** is true when the caller passed a block. 2. **`enum_for(name, *args)`** builds an Enumerator whose iteration calls `self.name(*args)` with a block that feeds the Enumerator. 3. **`__method__`** returns the current method's name as a Symbol, so the call stays correct after a rename. 4. **The arguments** must be passed through. `enum_for(__method__)` alone would call `each_tick` with no symbol and raise `ArgumentError` when iterated. Now every Enumerable method works on the result: ```ruby feed.each_tick("ACME").first(3) feed.each_tick("ACME").each_slice(100).map { |batch| batch.size } feed.each_tick("ACME").lazy.select { |t| t.price > 100 }.first(5) ``` ## to_enum vs enum_for | | `to_enum` | `enum_for` | |---|---|---| | Defined in | `Kernel` | `Kernel` | | Implementation | `obj_to_enum` in `enumerator.c` | the same `obj_to_enum` | | Default method | `:each` | `:each` | | Size block | accepted | accepted | They are interchangeable; pick one per codebase. `enum_for` reads better with a method name ("an enumerator for `each_tick`"), `to_enum` better on its own (`array.to_enum`). ## Sizing the Enumerator Without extra help, `feed.each_tick("ACME").size` returns `nil`, because Ruby cannot know how many times the method will yield without running it. If the method can compute the count cheaply, give `enum_for` a block: ```ruby def each_window(n) return enum_for(__method__, n) { [@ticks.size - n + 1, 0].max } unless block_given? @ticks.each_cons(n) { |w| yield w } end ``` - The block runs only when someone calls `size`. - It should return an Integer, `Float::INFINITY` for an endless iterator, or `nil` when unknown. - The rdoc calls size a **hint**: nothing checks it against the real count, so a wrong block silently lies. ## Behaviour worth knowing - **Re-execution.** The Enumerator calls the method again on every iteration. If the method fetches from a network API, each `first(3)` issues new requests. Materialise with `to_a` when you need the data twice. - **Protecting a collection.** `some_method(array.to_enum)` passes an iterable view that has no `<<` or `delete`, so the callee cannot mutate the array through it; the rdoc gives this example. - **Return value.** With a block, the method returns whatever its last expression returns; many authors return `self` to match `each`. - **Custom collections.** A class that includes `Enumerable` applies the same guard to its `each`; that pattern is covered with custom collection classes. ## Checklist - Guard with `return enum_for(__method__, *args) unless block_given?`. - Pass every argument through. - Add a size block only when the count is cheap and exact. - Remember that iterating the Enumerator re-runs the method.
- What goes wrong if you write enum_for(:each_tick) without passing the symbol argument?The Enumerator is created fine, but when iterated it calls `each_tick` with no arguments, which raises `ArgumentError` (wrong number of arguments). The failure appears far from the bug, at the first iteration.
- Why might iterating the returned Enumerator twice hit a remote API twice?An Enumerator stores the receiver, method and arguments, not results. Each `each`, `first` or `to_a` calls the method again, so any I/O inside it repeats. Call `to_a` once if the data must be reused.
saying these in an interview costs you the question
- to_enum is eager while enum_for is lazy
- enum_for(__method__) is enough even when the method takes arguments
- Without a size block, Enumerator#size iterates to count
- The Enumerator caches the yielded values after the first iteration
- A method that yields without a block simply returns nil