In Ruby, what do each, map and other Enumerable methods return when called without a block, and how is that used?
answer
- an object, not an error
- remembers receiver, method and arguments
- chain a second iterator onto it
- the first method decides the result
- size without iterating, or nil
basics
~20 sMost iterating methods return an Enumerator when no block is given: an object that remembers the receiver, the method and its arguments and runs them later. You chain it (each_with_index.map), iterate it elsewhere, or pull values with next.
solid answer
~40 sCalled without a block, `each`, `map`, `select`, `each_slice(2)`, `each_with_index`, `times`, `each_char` and most other iterators return an `Enumerator` instead of raising. The Enumerator stores the receiver, the method name and its arguments, and calls that method only when someone iterates it. That is what makes chains such as `ticks.each_with_index.map { |t, i| ... }` or `ticks.each_slice(100).map { |batch| ... }` work: the second method's block receives what the first method yields, and the **first** method decides the final return value (`map.with_index` returns the mapped Array, `select.with_index` the selected elements). An Enumerator can also report `size` without iterating when the method knows it (`[1, 2, 3].each.size` is 3, `loop.size` is `Infinity`), or `nil` when it cannot.
code
ruby · 11 linesTick = Data.define(:symbol, :price)
ticks = [Tick.new("ACME", 101), Tick.new("INIT", 99), Tick.new("ACME", 103)]
e = ticks.each_with_index # no block: an Enumerator
p e.size # => 3, computed without iterating
p e.map { |t, i| "#{i}:#{t.symbol}" }
# => ["0:ACME", "1:INIT", "2:ACME"]
p ticks.each_slice(2).map { |batch| batch.sum { |t| t.price } }
# => [200, 103]go deeper
Recall that each, map and select return an Enumerator without a block, and that you can chain another iterator such as map onto it.
Explain what an Enumerator stores, why the first method decides the chain's return value, and how size is answered without iterating.
Use blockless iterators to expose iteration as a value in APIs, and know which consumers are safe on an endless source.
Prefer returning Enumerators from library APIs so callers choose eager, lazy or external iteration without extra API surface.
## An Enumerator instead of an error In Ruby, a method that iterates with a block usually has a second, blockless form. Calling it without a block returns an **`Enumerator`**: ```ruby [1, 2, 3].each # => #<Enumerator: [1, 2, 3]:each> [1, 2, 3].map # => #<Enumerator: [1, 2, 3]:map> 5.times # => #<Enumerator: 5:times> "abc".each_char # => #<Enumerator: "abc":each_char> ``` An Enumerator is a small object holding three things: - the **receiver** (the array, the string, the integer); - the **method name** to call on it (`:each`, `:map`, `:each_slice`); - the **arguments** for that method (for example `2` in `each_slice(2)`). Nothing is iterated when the Enumerator is created. The method runs only when the Enumerator is iterated, and it runs **again** every time the Enumerator is iterated; an Enumerator is a recipe, not a snapshot. Core methods written in C produce these through the `RETURN_SIZED_ENUMERATOR` macro in `enum.c` and `array.c`; your own methods do it with `to_enum`/`enum_for`. ## Chaining iterators Because an Enumerator includes `Enumerable`, you can call another iterating method on it. The inner method's yields become the outer block's arguments: ```ruby %w[a b c].each_with_index.map { |s, i| s * (i + 1) } # => ["a", "bb", "ccc"] (1..7).each_slice(3).map { |batch| batch.sum } # => [6, 15, 7] ``` A rule worth stating in an interview: **the method that created the Enumerator decides what the chain returns**, because the chained block's result is fed back to it. | Chain | Block result used as | Return value | |---|---|---| | `each.with_index { ... }` | ignored, like `each` | the receiver | | `map.with_index { ... }` | the new element | a mapped Array | | `select.with_index { ... }` | a keep/drop verdict | the kept elements | | `each_with_index.map { ... }` | the new element | a mapped Array | ## Size without iterating `Enumerator#size` asks the method for a size **without** running the iteration, when the method can compute one: 1. `[1, 2, 3].each.size` returns `3`. 2. `(1..3).each_slice(2).size` returns `2`. 3. `loop.size` returns `Float::INFINITY`. 4. `(1..100).drop_while.size` returns `nil`, because the answer depends on the block. The rdoc treats `size` as a **hint**: an Enumerator built with an explicit size is not checked against what it yields. ## Where the stock-tick feed uses this A tick feed object that yields ticks from `each_tick` can hand callers an Enumerator instead of a callback API: ```ruby ticks = feed.each_tick # no block: an Enumerator ticks.first(500) # stops after 500 ticks ticks.find { |t| t.price > 100 } # stops at the first match ``` `first` and `find` stop iterating as soon as they have an answer, so a caller can consume only what it needs. For an endless feed, only such short-circuiting methods, `first(n)`, external `next`, or a `lazy` chain are safe; `map` or `to_a` would never return. ## Common misunderstandings - **"map without a block raises."** It returns an Enumerator. The exception is `Enumerator::Lazy`: `[1, 2, 3].lazy.map` raises `ArgumentError` (`tried to call lazy map without a block`). - **"The Enumerator holds a copy of the data."** It holds a reference to the receiver; mutate the array and the next iteration sees the change. - **"Every method has a blockless form."** Methods whose result is a single value computed from a block argument, such as `sum` or `count`, just compute the value; they do not return an Enumerator. ## Summary A blockless iterator returns an `Enumerator` that remembers what to call. Use it to chain a second iterator, to pass iteration around as a value, to ask for `size` cheaply, or to iterate externally with `next`.
- Why does map.with_index return an Array while each.with_index returns the receiver?The chained block's return value is handed back to the method that created the Enumerator. `map` collects those values into a new Array; `each` ignores them and returns its receiver. The Enumerator only relays yields and results between the two.
- If you build e = ticks.each and then push a tick onto the array, does e see it?Yes. The Enumerator holds a reference to the array and calls `each` on it each time it is iterated, so a later iteration sees the appended tick. It is a recipe for iteration, not a copy.
saying these in an interview costs you the question
- Calling map without a block raises ArgumentError
- An Enumerator stores a copy of the collection's elements
- each.with_index returns a new Array like map does
- Enumerator#size iterates the whole collection to count it
- An Enumerator can be iterated only once