Ruby 3.2引入Enumerator::product

又到了每年新版本Ruby发布的时候。让我们仔细看看一个新的Enumerator方法:::product

方法语法

这个更改的讨论线程在这里:https://bugs.ruby-lang.org/issues/18685

要理解这个新的Enumerator::product方法做什么,想象你想从两个数组获取笛卡尔积:

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

# 笛卡尔积
[[1, :a], [1, :b], [1, :c], [2, :a], [2, :b], [2, :c], [3, :a], [3, :b], [3, :c]]

在Ruby 3.2之前要做到这一点,需要嵌套数组。这是来自这个StackOverflow答案的解决方案:

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]]

从Ruby 3.2开始,我们可以这样做:

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]]

实际应用

让我们看看如何使用这个方法。假设我们正在构建一个电商应用,需要为T恤创建一个选项数组。

T恤会有以下选项:

  • 尺寸(小号、中号、大号)
  • 颜色(蓝色、红色、绿色、黄色)
  • 面料(棉、尼龙)

使用Enumerator::product,我们可以快速构建一个选项数组:

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"],
#   ...
# ]

太棒了!现在我们不需要遍历所有不同的组合来得到正确的选项。

深入了解

一些读者可能也注意到最近的Ruby版本已经包含了Array#product方法。这个方法定义在这里: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

然而,新的Enumerable::product方法的实现是不同的,它用C实现。那段代码太长无法粘贴在这里,所以这是链接:https://github.com/ruby/ruby/blob/v3_2_0_rc1/enumerator.c#L3382