In Ruby, which Enumerator::Lazy calls stay lazy and which force evaluation, and when do you need force, first or eager?
answer
- lazy map and select return Lazy
- take is lazy, first is eager
- force is an alias of to_a
- eager hands back a plain Enumerator
- sort_by or group_by consumes everything
basics
~20 sOn Enumerator::Lazy, map, select, reject, take, take_while and similar return another Lazy and compute nothing; non-redefined methods such as first(n), to_a/force, find and include? run the chain. eager turns it back into a plain Enumerator.
solid answer
~40 s`enum.lazy` returns an `Enumerator::Lazy`, which redefines the chainable Enumerable methods (`map`, `select`/`filter`, `reject`, `filter_map`, `flat_map`, `grep`, `take`, `take_while`, `drop`, `drop_while`, `zip`, `uniq`, `compact`, `with_index`) to return another Lazy without doing work. Any method it does not redefine runs the chain, one element at a time through every stage: `first(n)` stops after n results, `to_a` (alias `force`) collects everything, `find`, `include?` and `each` with a block stop or run as they normally would. So `lazy.map { }.take(3)` is still a Lazy and needs `force` or `first(3)`. `eager` converts a Lazy into a plain Enumerator, so a method can return a lazily built pipeline while callers' `select` or `map` return Arrays. Lazy adds per-element overhead, so it pays off on large, endless or expensive sources, not small arrays.
code
ruby · 16 linesprices = Enumerator.produce(100) { |price| price + [-3, 1, 4].sample }
chain = prices.lazy.select { |price| price > 105 }.map { |price| price.round(-1) }
p chain.class # => Enumerator::Lazy, nothing computed
p chain.take(2).class # => Enumerator::Lazy, take is lazy too
p chain.first(2) # e.g. [110, 110], stops after two matches
plain = chain.eager
p plain.class # => Enumerator
p plain.first(2).class # => Array
begin
[1, 2, 3].lazy.map
rescue ArgumentError => e
puts e.message # tried to call lazy map without a block
endgo deeper
Recall that lazy delays work until something like first or to_a asks for results, and that it lets you work with endless sequences.
Explain which methods return Lazy and which force it, the take versus first trap, and that force is an alias of to_a.
Spot hidden full scans such as sort_by or group_by inside lazy chains on endless feeds, and use eager when returning pipelines from methods.
Weigh lazy pipelines against batch processing for memory and latency budgets, and keep them where early termination or endless input justifies the overhead.
## What lazy changes Calling `lazy` on any Enumerable returns an **`Enumerator::Lazy`**. The class redefines most chainable Enumerable methods so that, instead of building an intermediate Array, each call just adds a stage to a pipeline. Nothing runs until something asks for values. ```ruby feed = tick_feed(client, "ACME") # endless Enumerator spikes = feed.lazy .map { |t| [t, t.price - t.prev_close] } .select { |_, move| move.abs > 5 } # => #<Enumerator::Lazy: ...> (no ticks fetched yet) spikes.first(3) # fetches pages only until three spikes are found ``` ## Stays lazy vs forces evaluation | Stays lazy: returns `Enumerator::Lazy` | Forces evaluation | |---|---| | `map`/`collect`, `flat_map` | `first` / `first(n)` | | `select`/`filter`/`find_all`, `reject`, `filter_map` | `to_a`, alias `force` | | `grep`, `grep_v` | `each` with a block | | `take`, `take_while`, `drop`, `drop_while` | `find`, `include?`, `any?` | | `zip`, `uniq`, `compact`, `with_index` | `reduce`, `sum`, `count`, `tally` | | `chunk_while`, `slice_when`, `slice_before`, `slice_after` | `sort`, `sort_by`, `group_by`, `min_by` | The rule behind the table, from the rdoc: real enumeration happens **when any non-redefined Enumerable method is called**. Some forcing methods short-circuit (`first`, `find`, `include?`, `any?`), so they are safe on an endless source once a match exists. Others must see every element (`to_a`, `sum`, `sort_by`, `group_by`), so on an endless source they never return. ## take vs first This pair is the most common interview trap: 1. `lazy.take(3)` is **lazy**: it returns a Lazy that will stop after three elements. You still need `force`, `to_a` or `each`. 2. `lazy.first(3)` is **eager**: it runs the chain and returns an Array of up to three elements. The rdoc's own example writes `take(10).force` next to `first(10)` with the comment "take is lazy, so force is needed". ## eager `Enumerator::Lazy#eager` returns a **non-lazy** Enumerator wrapping the lazy chain. Use it when a method builds a lazy pipeline internally but should return something that behaves like a normal Enumerator: ```ruby def liquid_ticks tick_feed(client, "ACME").lazy.reject { |t| t.volume.zero? }.eager end liquid_ticks.find { |t| t.price > 100 } # still stops early liquid_ticks.first(20) # an Array ``` Without `eager`, a caller's `liquid_ticks.map { ... }` would return another Lazy and silently compute nothing, a confusing bug in code that expected an Array. ## Rules and costs - **Blocks are mandatory.** `[1, 2, 3].map` returns an Enumerator, but `[1, 2, 3].lazy.map` raises `ArgumentError` (`tried to call lazy map without a block`). - **Per-element overhead.** Each element travels through every stage via Ruby-level calls, so on a small, finite Array `lazy` is usually slower than the eager chain. Its benefits are stopping early and not allocating intermediate Arrays. - **Re-execution.** A Lazy is a recipe; forcing it twice runs the source twice. - **Side effects interleave.** Stages run element by element, so a logging `map` followed by `select` logs one element, filters it, then logs the next, unlike the stage-by-stage order of an eager chain. ## When to reach for lazy - The source is **endless** (a tick feed, `loop`, `Enumerator.produce`, an endless range). - Each element is **expensive** (an HTTP call, parsing a large record) and you need only a few results. - The source is **large** and an eager chain would build big intermediate Arrays. ## Summary Lazy methods build a pipeline; non-redefined methods run it. Remember `take` stays lazy while `first` forces, `force` is `to_a`, and `eager` hands callers a normal Enumerator.
- Why does feed.lazy.select { ... }.sort_by { ... }.first(5) hang on an endless feed?`sort_by` is not redefined on `Enumerator::Lazy`, so calling it forces evaluation, and a sort must see every element before it can return anything. On an endless source it never finishes; the later `first(5)` is never reached.
- Is a lazy chain always faster than the eager version?No. Each element passes through every stage via extra calls, so on small finite collections the eager chain usually wins. Lazy pays off when you stop early, when the source is endless, or when avoiding large intermediate Arrays matters.
saying these in an interview costs you the question
- lazy.take(3) returns an Array of three elements
- force evaluates only the first element of a lazy chain
- eager evaluates the whole chain immediately
- sort_by on a lazy chain stays lazy
- Adding lazy always makes an Enumerable chain faster