rubyguides

Array#product

The .product method returns the cartesian product of arrays — all possible combinations of elements from each array. It produces a new array containing every possible pairing.

colors = [:red, :blue]
sizes = [:small, :large]

colors.product(sizes)
# => [[:red, :small], [:red, :large], [:blue, :small], [:blue, :large]]

This opening example shows the basic shape of the result: every item from the first array is paired with every item from the second array. That makes the method easy to reason about when you want to build a small combination table without nested loops.

Syntax

array.product(*other_arrays)
array.product(*other_arrays) { |combination| block }

The syntax is short because the method does one thing very clearly: you hand it one or more arrays and get back every possible combination as an array of arrays. The block form streams results one at a time instead of allocating a single large return array, which is worth remembering when the input arrays are long and the product would produce many entries. Understanding this signature is the first step toward using the method effectively.

Parameters

ParameterTypeDefaultDescription
*other_arraysArrayRequiredOne or more arrays to combine with

Once you understand the parameter signature, the next logical question is what shape the result takes. The return value is always a new array of combinations, and the nesting follows a consistent pattern regardless of how many input arrays you pass.

What .product Returns

.product returns a new array containing all possible combinations. Each combination is itself an array.

[1, 2].product([:a, :b])
# => [[1, :a], [1, :b], [2, :a], [2, :b]]

When called with no arguments, product returns an array of single-element arrays — each original element gets wrapped in its own one-item sub-array. This is a small corner case, but it keeps the return type consistent: you always get an array of arrays, never a bare array of scalars. That predictability is useful in generic code that builds results from a variable number of inputs.

[1, 2, 3].product
# => [[1], [2], [3]]

The no-argument case is a small edge case, but it is still useful to know. It shows that product treats the receiver itself as the source of combinations when there is nothing else to pair it with. That behavior can be handy in generic code that builds results from a variable number of inputs.

Common use cases

Generating all combinations

# Classic use: all size/color combinations
sizes = [:xs, :s, :m, :l, :xl]
colors = [:black, :white, :red]

sizes.product(colors)
# => [[:xs, :black], [:xs, :white], [:xs, :red], ... total 15 combinations]

This basic example covers the two-array case, which is the most common usage in everyday code. Each element from the first array is paired with every element from the second, giving you a full grid of options in a single method call. The shape of the result is predictable: an array of two-element arrays, one per combination.

Multiple Arrays

# Three or more arrays
[1, 2].product([:a, :b], [:x, :y])
# => [[1, :a, :x], [1, :a, :y], [1, :b, :x], [1, :b, :y],
#     [2, :a, :x], [2, :a, :y], [2, :b, :x], [2, :b, :y]]

Multiple arrays show the cartesian nature of the method very clearly because the output grows across every dimension at once. That makes the result predictable, but it also means the size can grow fast when more arrays are added.

With a Block

When given a block, .product yields each combination and returns nil:

[1, 2].product([:a, :b]) do |num, letter|
  puts "#{num}#{letter}"
end
# Output:
# 1a
# 1b
# 2a
# 2b
# => nil

The block form is worth using when you want to stream the combinations instead of storing them all at once, which can reduce memory pressure and make the code easier to hook into a processing pipeline. Testing is one of the most common applications of this pattern because you can generate every test configuration without building a large result array up front.

Practical example: test combinations

browsers = [:chrome, :firefox, :safari]
oses = [:windows, :macos, :linux]
versions = [100, 101, 102]

test_cases = browsers.product(oses, versions)
# Generate 27 test case combinations for automated testing

This is a classic testing pattern because the method makes it easy to build a full matrix of inputs. Once the combinations exist, the test runner can loop over them or feed them into parameterized checks without any extra hand-written nesting.

Creating a Grid

rows = 3
cols = 4

grid = (1..rows).to_a.product((1..cols).to_a)
# => [[1,1], [1,2], [1,3], [1,4], [2,1], ... [3,4]]

The grid example shows that product is not limited to small lookup tables — it also works well with ranges turned into arrays, making it a useful building block for coordinate pairs, layout calculations, and any problem that needs to enumerate every combination of two or more independent dimensions. When the result set grows large, the performance considerations below become especially important to keep in mind.

Performance Notes

  • The number of combinations grows exponentially: array1.size * array2.size * ...
  • For large arrays, this can create massive result sets
  • Use .each with blocks for memory-efficient iteration over combinations

These notes are the reason the method should be used deliberately. The output size is easy to calculate, but it can still surprise you if the inputs grow. Thinking about the size up front helps keep a convenient helper from turning into a heavy computation.

Practical Notes

product is useful when the problem is really about combinations rather than simple iteration. Test matrices, configuration grids, and catalog-style option lists are all good fits because the method makes every pairing explicit. That can save a lot of nested loop code and make the intended result much easier to review.

Because the number of results grows quickly, it is worth checking the size of the inputs before calling product on large collections. A small helper that estimates the result count can prevent a combinatorial blow-up in scripts that assemble many variants at once.

Gotchas

Forgetting the Result

# Common mistake: not capturing the return value
[1, 2].product([:a, :b]) { |combo| puts combo }
# => nil (block returns nil, not the combinations!)

# Fix: use the return value
result = [1, 2].product([:a, :b])
# => [[1, :a], [1, :b], [2, :a], [2, :b]]

The gotcha here is mostly about remembering that the block form is for side effects, not for collecting the result. If you need the array of combinations afterward, keep the return value instead of relying on the block output.

Empty Arrays

[1, 2].product([])
# => [] — empty array means no combinations

[].product([:a, :b])
# => [] — empty receiver means no combinations

Empty inputs are a nice example of the method being strict but unsurprising. No values means no pairings, so the empty array result is exactly what you would expect from the cartesian product definition.

See Also