skip to content

In Ruby, what do each, map and other Enumerable methods return when called without a block, and how is that used?

level: juniorimportance: must knowfreq 55%

answer

  1. an object, not an error
  2. remembers receiver, method and arguments
  3. chain a second iterator onto it
  4. the first method decides the result
  5. size without iterating, or nil

basics

~20 s

Most 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 s

Called 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 lines
ruby
Tick = 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

for a junior

Recall that each, map and select return an Enumerator without a block, and that you can chain another iterator such as map onto it.

for a middle

Explain what an Enumerator stores, why the first method decides the chain's return value, and how size is answered without iterating.

for a senior

Use blockless iterators to expose iteration as a value in APIs, and know which consumers are safe on an endless source.

for a principal

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