Gerando variantes de produtos em Ruby
Com base nas variantes de amostra, preciso obter todas as combinações de todas as variantes. No exemplo, tenho 3x3x2 = 18 variantes.
## SAMPLE VARIANTS
sizes = ['small', 'medium', 'large']
colors = ['red', 'green', 'blue']
materials = ['cotton', 'linen']
## ITERATE TO ALL VARIANTS
titles = []
sizes.each do |size|
colors.each do |color|
materials.each do |material|
## PUT THE VARIANT IN THE NEW ARRAY
titles.push("#{size} - #{color} - #{material}")
end
end
end
puts titles.inspect
Meu cada loop aninhado é preferido ou há alguma implementação melhor para isso?
Respostas
Literais de string congelados
Estruturas de dados imutáveis e código puramente funcional são sempre preferidos, a menos que mutabilidade e efeitos colaterais sejam necessários para clareza ou desempenho. No Ruby, as strings são sempre mutáveis, mas há um comentário mágico que você pode adicionar aos seus arquivos (também disponível como uma opção de linha de comando para o mecanismo Ruby), que tornará automaticamente todas as strings literais imutáveis:
# frozen_string_literal: true
Geralmente é preferível adicionar este comentário a todos os seus arquivos.
Literais de "porcentagem" da matriz de palavras
Ruby tem literais de array especiais para arrays de strings de uma única palavra que podem tornar seu código mais fácil de ler, reduzindo a quantidade de "fluff de sintaxe" em torno do conteúdo real.
O literal começa com os sigilos %wou %W(pense em "palavra" ou "separado por witespace"). %wse comporta como uma string entre aspas simples, ou seja, não realiza interpolação e não suporta caracteres de escape além de \'e \\. %Wse comporta como uma string entre aspas duplas.
Portanto, o início do seu script pode ser assim:
# frozen_string_literal: true
## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]
Como acontece com todos os literais de porcentagem, você pode escolher livremente o delimitador que deseja usar, de forma que o delimitador não ocorra dentro do literal. Por exemplo, você pode usar |como delimitador ,, @qualquer coisa que desejar:
sizes = %w@small medium large@
colors = %w@red green blue@
materials = %w@cotton linen@
O primeiro caractere após o wdetermina o delimitador. Os delimitadores vêm em duas variantes: emparelhados e não emparelhados. Com um delimitador desemparelhado, o mesmo caractere termina o literal, como no segundo exemplo. Com um delimitador pareado, o delimitador de fechamento correspondente termina o literal, por exemplo, quando você começa com <, fecha com >, etc. Veja o primeiro exemplo.
Fiapos
Você deve executar algum tipo de linter ou analisador estático em seu código. Rubocop é popular, mas existem outros.
O Rubocop foi capaz de detectar todas as melhorias de estilo que mencionei acima e também foi capaz de autocorreção de todas as que listei.
Eu configurei meu editor de forma que ele execute automaticamente o Rubocop com correção automática assim que eu clicar em "salvar".
Este é o resultado da correção automática:
# frozen_string_literal: true
## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]
## ITERATE TO ALL VARIANTS
titles = []
sizes.each do |size|
colors.each do |color|
materials.each do |material|
## PUT THE VARIANT IN THE NEW ARRAY
titles.push("#{size} - #{color} - #{material}")
end
end
end
puts titles.inspect
puts foo.inspect
Kernel#pé o método de depuração preferido. Ele faz a mesma coisa, mas é mais idiomático e é projetado especificamente para depuração rápida (daí o nome de um caractere).
Então, a última linha pode ser simplesmente
p titles
Além disso, Kernel#putsretorna nil, mas Kernel#pretorna seu (s) argumento (s), para que você possa colocá-lo em uma longa cadeia de expressões sem alterar o resultado.
Espaço em branco vertical
Seu código pode usar alguns espaços em branco verticais para dar ao código mais espaço para respirar. Eu sugeriria pelo menos separar a inicialização no início do loop:
titles = []
sizes.each do |size|
colors.each do |color|
materials.each do |material|
## PUT THE VARIANT IN THE NEW ARRAY
titles.push("#{size} - #{color} - #{material}")
end
end
end
O operador de "pá" <<
Array#pushnão é idiomático. Mais precisamente, é apenas idiomático se você estiver usando o array como uma pilha , então você usaria Array#pushe Array#pop, uma vez que esses são os nomes padrão para as operações de pilha.
A maneira idiomática de acrescentar algo a outra coisa é o operador de pá, neste caso Array#<<, de modo que deve ser
titles << "#{size} - #{color} - #{material}"
Iteradores
Em Ruby, é idiomático usar iteradores de alto nível. Em seu código, você já está usando iteradores em vez de loops, o que é bom. No entanto, eaché realmente o nível mais baixo de todos os iteradores. É essencialmente equivalente a um FOREACH-OFloop. Não tem semântica de nível superior e depende de mutações e efeitos colaterais.
Sempre que você tem o padrão de "Inicializar um resultado, fazer um loop sobre uma coleção anexando ao resultado, retornar o resultado", isso é uma dobra . Existem duas implementações de fold na biblioteca central do Ruby, injecte each_with_object. injecté o mais funcional, each_with_objecté o mais imperativo. Então, por enquanto, vamos usar each_with_objectaqui, já que o código ainda é muito importante, e isso torna a relação entre o código antigo e o novo mais clara.
Como uma transformação geral,
accumulator = some_initial_value
collection.each do |element|
accumulator = do_something_with(accumulator, element)
end
torna-se
accumulator = collection.inject(some_initial_value) do |accumulator, element|
do_something_with(accumulator, element)
end
ou
collection.each_with_object(some_initial_value) do |element, accumulator|
do_something_with(accumulator, element)
end
No seu caso, seria assim:
titles = []
sizes.each do |size|
colors.each do |color|
materials.each do |material|
## PUT THE VARIANT IN THE NEW ARRAY
titles << "#{size} - #{color} - #{material}"
end
end
end
torna-se
titles = []
sizes.each_with_object(titles) do |size, titles|
colors.each_with_object(titles) do |color, titles|
materials.each_with_object(titles) do |material, titles|
## PUT THE VARIANT IN THE NEW ARRAY
titles << "#{size} - #{color} - #{material}"
end
end
end
Certo, isso não nos compra muito, na verdade, o oposto. Mas começa a parecer um pouco diferente quando mudamos para uma versão puramente funcional, sem efeitos colaterais e mutação usando Enumerable#inject:
titles = sizes.inject([]) do |acc, size|
colors.inject(acc) do |acc, color|
materials.inject(acc) do |acc, material|
## PUT THE VARIANT IN THE NEW ARRAY
acc + ["#{size} - #{color} - #{material}"]
end
end
end
Linter, revisitado
Rubocop realmente reclama do meu uso de sombrear o exterior acccom o interior acc.
Discordo. Você não deve ter medo de desativar ou reconfigurar as regras em seu linter para se adequar ao seu estilo.
No entanto, observe que a programação é um esporte de equipe. Se você estiver modificando o código, adote o estilo existente. Se você faz parte de uma equipe, adote o estilo da equipe. Se você escreve código-fonte aberto, adote o estilo do projeto. Se você iniciar seu próprio projeto, adote o estilo da comunidade ( não crie seu próprio estilo para o seu projeto, até que ele seja grande e bem-sucedido o suficiente para ter sua própria comunidade independente).
Excursão: Generalidade da dobra ( inject/ each_with_object)
Quando eu escrevi acima que você pode reescrever essa iteração com injectou each_with_object, isso foi na verdade uma declaração tautológica. Nem precisei ler o código para fazer essa declaração.
Acontece que essa dobra é "geral". Cada iteração em uma coleção pode ser expressa usando fold . Isso significa que, se excluirmos todos os métodos de Enumerable, exceto inject, poderemos reimplementar todo o Enumerablemódulo novamente, usando nada além de inject. Enquanto tivermos inject, podemos fazer qualquer coisa.
Iteradores, pegue 2
Portanto, o que fizemos até agora foi substituir o iterador de nível inferior por um iterador de nível superior.
No entanto, ainda não terminamos. O que estamos fazendo agora é pegar cada três elementos de nossas três coleções, concatená-los e colocá-los em uma nova coleção. Então, realmente, o que estamos fazendo é transformar cada elemento (ou triplo de elementos), ou "mapear" cada elemento para um novo elemento.
Isso é chamado de mapa e também está disponível em Ruby as Enumerable#map.
Então, finalmente, nosso código se parece com este:
titles = sizes.map do |size|
colors.map do |color|
materials.map do | material|
"#{size} - #{color} - #{material}"
end
end
end
Na verdade, esse resultado não é muito correto: obtemos um array triplicadamente aninhado, porque temos um triplicamente aninhado Enumerable#map.
Poderíamos Array#flatteno resultado, mas existe uma maneira melhor Enumerable#flat_map::
titles = sizes.flat_map do |size|
colors.flat_map do |color|
materials.map do | material|
"#{size} - #{color} - #{material}"
end
end
end
O que fizemos aqui foi substituir a dobra geral do iterador de alto nível (que pode fazer qualquer coisa ) por um mapa do iterador de alto nível mais restrito e especializado . Usando um iterador mais especializado, somos capazes de transmitir melhor nossa semântica ao leitor. Em vez de pensar "Ok, então aqui temos um acumulador e um elemento, e fazemos algo com o elemento, e depois acrescentamos ao acumulador ... ah, entendo, estamos transformando cada elemento", o leitor apenas vê e sabe instantaneamente que transforma os elementos.mapmap
Métodos Array
Não há muito que possamos melhorar no código usando iteradores. Entretanto, existem muitos outros métodos tanto no Enumerablemixin quanto na Arrayclasse .
Portanto, vamos dar um passo para trás e pensar sobre o que estamos realmente fazendo aqui: estamos construindo o Produto Cartesiano dos três arrays. E talvez não seja surpreendente, já existe um método que calcula um produto de matrizes, criativamente denominado Array#product:
titles = sizes.product(colors, materials).map do |size, color, material|
"#{size} - #{color} - #{material}"
end
Array#join
Como última melhoria, vamos ver o que o bloco está fazendo: é "juntar" as três variantes. E, novamente, já existe um método que faz isso Array#join::
titles = sizes.product(colors, materials).map do |variant|
variant.join(' - ')
end
Resultado final
Então, no final, a coisa toda se parece com isto:
# frozen_string_literal: true
## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]
titles = sizes.product(colors, materials).map do |variant|
variant.join(' - ')
end
p titles
Acho que é um código bonito, fácil de ler e de entender.