Ruby 3.2 introduces Enumerator::product

Ruby 3.2 introduces Enumerator::product

Learn how to use the Enumerator::product method

ยท

2 min read

It is that time of the year again when a new version of Ruby is released. Let us take a closer look at a new Enumerator method: ::product.

Method Syntax

Here is the thread for this change: https://bugs.ruby-lang.org/issues/18685

To understand what this new Enumerator::product method does, imagine you want to get a Cartesian product from two arrays like so:

array_1 = [1, 2, 3]
array_2 = [:a, :b, :c]

# Cartesian product
[[1, :a], [1, :b], [1, :c], [2, :a], [2, :b], [2, :c], [3, :a], [3, :b], [3, :c]]

To be able to do this before Ruby 3.2, one would need to nest the arrays. Here is a solution taken from this StackOverflow answer:

class CartesianProduct
  include Enumerable

  def initialize(xs, ys)
    @xs = xs
    @ys = ys
  end

  def each
    return to_enum unless block_given?
    @xs.each do |x| 
      @ys.each { |y| yield [x, y] }
    end
  end
end

products = CartesianProduct.new(array_1, array_2).each.to_a
# [[1, :a], [1, :b], [1, :c], [2, :a], [2, :b], [2, :c], [3, :a], [3, :b], [3, :c]]

Beginning in Ruby 3.2, we can do this instead:

products = Enumerator.product(array_1, array_2).to_a
# [[1, :a], [1, :b], [1, :c], [2, :a], [2, :b], [2, :c], [3, :a], [3, :b], [3, :c]]

Practical Applications

Let's take a look at how we might use this method. Imagine we are building an e-commerce application, and we need to create an array of options for a T-shirt.

The T-shirt would have the following options:

  • Size (Small, Medium, Large)

  • Color (Blue, Red, Green, Yellow)

  • Fabric (Cotton, Nylon)

Using Enumerator::product, we can quickly construct an array of options like so:

colors = ['blue', 'red', 'green', 'yellow']
sizes = ['small', 'medium', 'large']
fabrics = ['cotton', 'nylon']

variants = Enumerator.product(colors, sizes, fabrics).to_a
# [
#   ["blue", "small", "cotton"],
#   ["blue", "small", "nylon"],
#   ["blue", "medium", "cotton"],
#   ["blue", "medium", "nylon"],
#   ["blue", "large", "cotton"],
#   ...
# ]

Nice! Now we don't need to iterate through all the different combinations to come up with the right options.

Digging Deeper

Some readers may also notice that recent Ruby versions already include an Array#product method. For this method, it is defined here: https://github.com/ruby/ruby/blob/v3_2_0_rc1/tool/transcode-tblgen.rb#L10-L20

def product(*args)
  if args.empty?
    self.map {|e| [e] }
  else
    result = []
    self.each {|e0|
      result.concat args.first.product(*args[1..-1]).map {|es| [e0, *es] }
    }
    result
  end
end

However, the newer Enumerable::product method's implementation is different and is implemented in C. That code is too long to paste in here, so here is the link: https://github.com/ruby/ruby/blob/v3_2_0_rc1/enumerator.c#L3382.

ย