A Ruby job builds every row, seat, price band and show date combination with Array#product and memory spikes; why, and what does passing a block change?
answer
- size is the product of the lengths
- result allocated up front
- RangeError: too big to product
- block form returns self
- one combination at a time
basics
~20 sWithout a block, Array#product builds one result array holding a new sub-array per combination, so memory grows with the product of all input sizes. With a block it yields each combination and returns the receiver, building no result array.
solid answer
~50 s`a.product(b, c)` returns every combination as an array of arrays. Its length is the **product** of the input lengths, and without a block Ruby allocates that result array up front and fills it with a new sub-array per combination, so 20 rows x 30 seats x 2 bands x 90 dates is 108,000 four-element arrays alive at once. If the lengths multiply past what an array can hold, it raises `RangeError` (`too big to product`). With a block, `product` yields each combination as it is built and **returns `self`**, so each sub-array can be collected once the block moves on; `result = rows.product(...) { }` gives you `rows`, not the combinations. An empty input gives `[]` or no block calls. Stream the combinations to their destination inside the block, in batches, rather than materialising them.
code
ruby · 14 linesrows = ("A".."T").to_a # 20
seats = (1..30).to_a # 30
bands = %w[standard premium] # 2
dates = (1..90).to_a # 90
all = rows.product(seats, bands, dates)
all.size # => 108000, every one a new 4-element array
count = 0
result = rows.product(seats, bands, dates) do |row, seat, band, date|
count += 1 # write this price row out here instead of keeping it
end
result.equal?(rows) # => true, the block form returns the receiver
count # => 108000go deeper
Recall that product returns every pairing across arrays, and that its size is the input sizes multiplied together.
Explain that the blockless form materialises all combinations while the block form yields them and returns the receiver.
Estimate combination counts before calling product in a job, stream through the block form, and catch code that assigns the block form's return value.
Judge when combinatorial generation belongs in application memory at all, and set size limits that fail early for jobs that multiply inputs.
## What Array#product computes `Array#product` returns the **Cartesian product** of the receiver and its arguments: every way of picking one element from each array. `[0, 1].product([2, 3])` is `[[0, 2], [0, 3], [1, 2], [1, 3]]`. With no arguments it returns one-element arrays, `[[0], [1]]`. The number of combinations is the **product of the sizes**, not the sum. That is the whole memory story. ## Why memory spikes Without a block, the implementation: 1. Multiplies the input lengths to get the result size, raising `RangeError` with `too big to product` if the multiplication overflows. 2. Allocates one result array of that size up front. 3. Builds a **new sub-array** for every combination and stores it. For a theatre pricing job: | Input | Size | |---|---| | rows `A`..`T` | 20 | | seats per row | 30 | | price bands | 2 | | show dates | 90 | | **combinations** | **108,000** | That is 108,000 four-element arrays plus a 108,000-slot result array, all reachable until the result goes out of scope. Add one more dimension, say 12 ticket types, and it becomes 1,296,000. Each extra input multiplies the cost. ## The block form With a block, `product` changes shape: - It **yields** each combination to the block as soon as it is built, one at a time. - It **does not build** the result array; after the block returns, the sub-array is garbage unless the block kept it. - It **returns `self`**, the receiver, so `x = rows.product(seats) { ... }` sets `x` to `rows`. - It skips the up-front size multiplication, so the `RangeError` check applies only to the blockless form. A block with several parameters receives the combination destructured: `rows.product(seats, bands, dates) { |row, seat, band, date| ... }`. ## Edge behaviours - **An empty input** makes the product empty: `[]` without a block, and no block calls with one. - **Order**: Ruby's documentation describes the order of combinations as indeterminate. Do not make correctness depend on it; sort afterwards if order matters. - **Not lazy**: without a block, `product` returns a fully built `Array`, not an `Enumerator`. ## product versus nested loops The block form is equivalent to nested `each` loops, one per input, with the combination packed into an array for you: ```ruby rows.each do |row| seats.each do |seat| # same pairs as rows.product(seats) { |row, seat| ... } end end ``` Nested loops let you skip a whole inner loop early, for example skipping every seat of a row that is closed for renovation, which `product` cannot do: its block can `break` out of the whole run, but it cannot skip one row's seats without being called for each of those combinations. Reach for `product` when every combination is genuinely needed and the dimension list is dynamic, such as `first.product(*rest)` over a list of option arrays. ## Production practice - **Estimate first.** Multiply the sizes before calling `product`; a harmless-looking fifth argument can turn thousands into millions. - **Stream.** Use the block form and write each combination, or small batches of them, to their destination inside the block. - **Keep only what you need.** If the job filters combinations, filter inside the block instead of building everything and discarding most of it. - **Pick the right method.** `product` pairs every element with every other element across arrays. `zip` pairs by index, and `combination(n)` picks subsets of one array; neither is a cheaper `product`.
- What does Array#product return when one of the arrays is empty?An empty product. Without a block it returns `[]`; with a block it never calls the block and returns the receiver. One empty dimension, such as a show with no dates yet, silently yields zero combinations rather than an error.
- When does Array#product raise RangeError, and does the block form raise it too?Without a block, `product` multiplies the input lengths to size its result and raises `RangeError` with `too big to product` when that overflows. The block form never computes the total, so it does not raise this error; it simply keeps yielding.
saying these in an interview costs you the question
- product returns a lazy enumerator, so it costs nothing until iterated
- The block form still returns the full array of combinations
- The number of combinations is the sum of the input sizes
- combination gives the same cross pairs as product with less memory
- zip pairs every seat with every date