Ruby 3.2 introduce Enumerator::product

Es esa época del año de nuevo cuando se lanza una nueva versión de Ruby. Echemos un vistazo más de cerca a un nuevo método de Enumerator: ::product.

Sintaxis del Método

Aquí está el hilo para este cambio: https://bugs.ruby-lang.org/issues/18685

Para entender qué hace este nuevo método Enumerator::product, imagina que quieres obtener un producto cartesiano de dos arrays así:

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

# Producto cartesiano
[[1, :a], [1, :b], [1, :c], [2, :a], [2, :b], [2, :c], [3, :a], [3, :b], [3, :c]]

Para poder hacer esto antes de Ruby 3.2, uno necesitaría anidar los arrays. Aquí hay una solución tomada de esta respuesta de 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]]

A partir de Ruby 3.2, podemos hacer esto en su lugar:

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

Aplicaciones Prácticas

Veamos cómo podríamos usar este método. Imagina que estamos construyendo una aplicación de comercio electrónico y necesitamos crear un array de opciones para una camiseta.

La camiseta tendría las siguientes opciones:

  • Talla (Pequeña, Mediana, Grande)
  • Color (Azul, Rojo, Verde, Amarillo)
  • Tela (Algodón, Nylon)

Usando Enumerator::product, podemos construir rápidamente un array de opciones así:

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

¡Genial! Ahora no necesitamos iterar a través de todas las diferentes combinaciones para obtener las opciones correctas.

Profundizando

Algunos lectores también pueden notar que las versiones recientes de Ruby ya incluyen un método Array#product. Para este método, está definido aquí: 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

Sin embargo, la implementación del método Enumerable::product más nuevo es diferente y está implementado en C. Ese código es demasiado largo para pegarlo aquí, así que aquí está el enlace: https://github.com/ruby/ruby/blob/v3_2_0_rc1/enumerator.c#L3382.