In Ruby, why do Enumerable#reverse_each and Enumerable#cycle buffer every element, and when do Array and Range avoid that?
answer
- each only walks forward, once
- reverse_each calls to_a first
- cycle replays a recorded copy
- Array walks indexes backwards
- endless Range raises TypeError
basics
~20 sA plain Enumerable only offers a forward, one-pass each, so reverse_each first collects everything with to_a and cycle records each element to replay later. Array walks its own indexes, and integer Ranges count down, so neither buffers.
solid answer
~40 s`Enumerable` knows only one thing about its host: `each`, which walks forward. To go backwards, `Enumerable#reverse_each` calls `to_a` and walks that array from the end, so a class streaming ten million rows is fully loaded before the first yield. `Enumerable#cycle` records every element into an internal array on the first pass and replays that copy, so a file-backed source is read once and later passes do not see changes. `Array#reverse_each` walks its own indexes backwards and allocates nothing, which also makes it cheaper than `reverse.each`, which builds a reversed copy. `Range#reverse_each` counts integer ranges down directly; since Ruby 3.3 it also handles beginless integer ranges and raises `TypeError` for endless ones. `cycle` returns `nil`, and without a count it repeats until the block breaks out.
code
ruby · 18 linesclass ResultFeed
include Enumerable
def initialize(path)
@path = path
end
def each
File.foreach(@path) { |line| yield line.chomp }
end
end
feed = ResultFeed.new("results.txt")
feed.reverse_each { |row| puts row } # reads the whole file into an array first
[1, 2, 3].reverse_each { |n| print n } # prints 321, no copy
[1, 2].cycle(2) { |n| print n } # prints 1212, returns nil
(1..).reverse_each { } # TypeError: can't iterate from NilClassgo deeper
Recall that reverse_each walks backwards and returns the receiver, and that cycle repeats a sequence and returns nil.
Explain that the generic reverse_each builds an array with to_a first, while Array#reverse_each walks indexes and reverse.each makes a copy.
Check which receiver a reverse_each or cycle call lands on in a streaming job, and override the method in a custom class rather than paying for to_a.
Weigh whether a collection abstraction should promise only forward iteration, and document the cost of operations that need random access or several passes.
## What Enumerable can and cannot do The `Enumerable` module gives a class dozens of methods in exchange for one: **`each`**, which yields the elements from first to last. That contract says nothing about going backwards, jumping to the end, or walking twice. A class may stream rows from a socket or a file, and its `each` might not even be repeatable. Two each-style methods need more than one forward pass, so their generic versions **buffer**: - **`Enumerable#reverse_each`** calls `to_a`, then yields from the last index down. The C code in `enum.c` literally starts with `ary = enum_to_a(...)`. - **`Enumerable#cycle`** records each element into a hidden array while yielding it on the first pass, then replays that array for later passes. ## What buffering costs For a small collection nothing. For a large or streaming one it matters: 1. **Memory.** `reverse_each` on a class streaming ten million result rows holds all ten million before yielding the first. 2. **Latency.** Nothing is yielded until the whole source has been read. 3. **Staleness.** `cycle` never calls `each` again, so if the underlying file or feed changes after the first pass, later passes still replay the first pass's elements. ## Classes that override them Concrete collections know their own layout and override the generic versions: | Receiver | `reverse_each` strategy | Buffers? | |---|---|---| | `Array` | walks its indexes from the end | no | | `Range` of integers | counts down from the end value | no | | `Range` of other values | falls back to `Enumerable#reverse_each` | yes | | Custom `Enumerable` | `to_a`, then walks backwards | yes | `Array#cycle` is also native: it loops over the array's own storage and needs no copy. On arrays, `reverse_each` is also the better choice over `reverse.each`. The latter builds a **reversed copy** of the whole array first, then walks it. `reverse_each` allocates nothing extra and returns the original array. ## Ranges and the Ruby 3.3 change Ruby 3.3's release notes record two changes to `Range#reverse_each`: - It can now process **beginless** ranges with an Integer end, such as `(..5)`, counting down forever unless the block breaks. - It now raises **`TypeError`** for **endless** ranges such as `(1..)`, because there is no end to start from. Before 3.3, the call fell through to the generic version, which tries to collect every element of an infinite range first and so never finishes. ## `cycle` in practice `cycle(n)` repeats the whole sequence `n` times; with no argument or `nil`, it repeats **forever**. It always returns **`nil`** when given a block. A count of zero or less, or an empty receiver, means the block never runs. A marathon organiser assigning pacer groups in rotation might write: ```ruby groups = %w[A B C].cycle runners.each { |r| r.assign(groups.next) } ``` Here `cycle` is called without a block, so it returns an enumerator whose `next` hands out `A`, `B`, `C`, `A` and so on. `zip` with the cycled enumerator is another common form. ## Allocation, not just size Even on arrays the choice of form matters for allocation. `results.reverse.each` allocates one extra array the size of `results`; `results.reverse_each` allocates none. For a single call on a small array this is noise, but inside a request that runs thousands of times per minute, or on arrays of millions of rows, the extra allocation shows up as garbage-collector work. ## Judgement in review The production-level point is to know **which receiver** a call lands on: - On an `Array` or integer `Range`, `reverse_each` and `cycle` are cheap. - On a custom `Enumerable` or a streaming source, they silently materialise everything, which can turn a bounded-memory job into one that grows with input size. - If a streaming class genuinely needs reverse iteration, it should define its own `reverse_each` that uses what it knows, such as reading a file from the end.
- How would you make reverse_each cheap for a custom streaming class?Define `reverse_each` on the class itself. `Enumerable#reverse_each` is only a fallback, and a method defined in the class takes precedence over the module's. The class can use what it knows, for example reading a file backwards in blocks, instead of buffering every row through `to_a`.
- What does cycle return, and how do you stop an infinite cycle?With a block, `cycle` returns `nil` once it stops, which happens after `n` passes or immediately for a count of zero or less or an empty receiver. With no count it repeats forever, so you either pass a count, `break` out of the block, or call `cycle` without a block and pull values with `next` or `zip`.
saying these in an interview costs you the question
- Enumerable#reverse_each walks the source backwards without copying it
- arr.reverse.each is as cheap as arr.reverse_each
- cycle re-reads the source on every pass, so it sees new data
- cycle returns the receiver like each does
- (1..).reverse_each raises RangeError in Ruby 4.0