Para fazer o equivalente às compreensões de lista Python, estou fazendo o seguinte:
some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}
Existe uma maneira melhor de fazer isso ... talvez com uma chamada de método?
Para fazer o equivalente às compreensões de lista Python, estou fazendo o seguinte:
some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}
Existe uma maneira melhor de fazer isso ... talvez com uma chamada de método?
Respostas:
Se você realmente quiser, pode criar um método Array # compreender como este:
class Array
def comprehend(&block)
return self if block.nil?
self.collect(&block).compact
end
end
some_array = [1, 2, 3, 4, 5, 6]
new_array = some_array.comprehend {|x| x * 3 if x % 2 == 0}
puts new_array
Impressões:
6
12
18
Eu provavelmente faria do jeito que você fez.
[nil, nil, nil].comprehend {|x| x }
que retorna []
.
compact!
retorna nil em vez da matriz quando nenhum item é alterado, então não acho que funcione.
Que tal:
some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact
Um pouco mais limpo, pelo menos para o meu gosto, e de acordo com um teste rápido de benchmark cerca de 15% mais rápido que a sua versão ...
some_array.map{|x| x * 3 unless x % 2}.compact
, que é indiscutivelmente mais legível / ruby-esque.
unless x%2
não tem efeito porque 0 é verdadeiro em rubi. Consulte: gist.github.com/jfarmer/2647362
Fiz um rápido benchmark comparando as três alternativas e o map-compact realmente parece ser a melhor opção.
require 'test_helper'
require 'performance_test_help'
class ListComprehensionTest < ActionController::PerformanceTest
TEST_ARRAY = (1..100).to_a
def test_map_compact
1000.times do
TEST_ARRAY.map{|x| x % 2 == 0 ? x * 3 : nil}.compact
end
end
def test_select_map
1000.times do
TEST_ARRAY.select{|x| x % 2 == 0 }.map{|x| x * 3}
end
end
def test_inject
1000.times do
TEST_ARRAY.inject([]) {|all, x| all << x*3 if x % 2 == 0; all }
end
end
end
/usr/bin/ruby1.8 -I"lib:test" "/usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/performance/list_comprehension_test.rb" -- --benchmark
Loaded suite /usr/lib/ruby/gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
ListComprehensionTest#test_inject (1230 ms warmup)
wall_time: 1221 ms
memory: 0.00 KB
objects: 0
gc_runs: 0
gc_time: 0 ms
.ListComprehensionTest#test_map_compact (860 ms warmup)
wall_time: 855 ms
memory: 0.00 KB
objects: 0
gc_runs: 0
gc_time: 0 ms
.ListComprehensionTest#test_select_map (961 ms warmup)
wall_time: 955 ms
memory: 0.00 KB
objects: 0
gc_runs: 0
gc_time: 0 ms
.
Finished in 66.683039 seconds.
15 tests, 0 assertions, 0 failures, 0 errors
reduce
neste benchmark também (consulte stackoverflow.com/a/17703276 ).
inject
==reduce
Parece haver alguma confusão entre os programadores Ruby neste tópico sobre o que é compreensão de lista. Cada resposta assume alguma matriz preexistente para transformar. Mas o poder da compreensão de lista está em uma matriz criada instantaneamente com a seguinte sintaxe:
squares = [x**2 for x in range(10)]
O seguinte seria um análogo em Ruby (a única resposta adequada neste tópico, AFAIC):
a = Array.new(4).map{rand(2**49..2**50)}
No caso acima, estou criando uma matriz de inteiros aleatórios, mas o bloco pode conter qualquer coisa. Mas isso seria uma compreensão de lista Ruby.
Eu discuti este tópico com Rein Henrichs, que me disse que a solução de melhor desempenho é
map { ... }.compact
Isso faz sentido porque evita a construção de Arrays intermediários, como ocorre com o uso imutável de Enumerable#inject
, e evita o crescimento do Array, o que causa a alocação. É tão geral quanto qualquer um dos outros, a menos que sua coleção possa conter elementos nulos.
Eu não comparei isso com
select {...}.map{...}
É possível que a implementação em C do Ruby Enumerable#select
também seja muito boa.
Uma solução alternativa que funcionará em todas as implementações e rodará em tempo O (n) em vez de O (2n) é:
some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
2
coisas n
vezes em vez de 1
coisas n
vezes e depois outra 1
coisa n
vezes :) Uma vantagem importante de inject
/ reduce
é que ele preserva quaisquer nil
valores na sequência de entrada, que é um comportamento mais abrangente de lista
Acabei de publicar a gem de compreensão para RubyGems, que permite que você faça isso:
require 'comprehend'
some_array.comprehend{ |x| x * 3 if x % 2 == 0 }
Está escrito em C; a matriz é percorrida apenas uma vez.
Enumerable tem um grep
método cujo primeiro argumento pode ser um predicado proc e cujo segundo argumento opcional é uma função de mapeamento; então o seguinte funciona:
some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}
Isso não é tão legível quanto algumas outras sugestões (eu gosto da gema simples de select.map
anoiaque ou de compreensão de histocrata), mas seus pontos fortes são que ela já faz parte da biblioteca padrão, é de passagem única e não envolve a criação de matrizes intermediárias temporárias , e não requer um valor fora dos limites, como nil
usado nas compact
sugestões -using.
Isso é mais conciso:
[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Como o Pedro mencionou, você pode fundir as chamadas encadeadas para Enumerable#select
e Enumerable#map
, evitando uma passagem sobre os elementos selecionados. Isso é verdade porque Enumerable#select
é uma especialização de dobra ou inject
. Eu postei uma introdução apressada ao tópico no subreddit Ruby.
A fusão manual das transformações de Array pode ser entediante, então talvez alguém possa brincar com a comprehend
implementação de Robert Gamble para tornar este select
/ map
padrão mais bonito.
Algo assim:
def lazy(collection, &blk)
collection.map{|x| blk.call(x)}.compact
end
Chame-o:
lazy (1..6){|x| x * 3 if x.even?}
Que retorna:
=> [6, 12, 18]
lazy
em Array e então:(1..6).lazy{|x|x*3 if x.even?}
Esta é uma maneira de abordar isso:
c = -> x do $*.clear
if x['if'] && x[0] != 'f' .
y = x[0...x.index('for')]
x = x[x.index('for')..-1]
(x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
x.insert(x.length, "end; $*")
eval(x)
$*)
elsif x['if'] && x[0] == 'f'
(x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << x")
x.insert(x.length, "end; $*")
eval(x)
$*)
elsif !x['if'] && x[0] != 'f'
y = x[0...x.index('for')]
x = x[x.index('for')..-1]
(x.insert(x.index(x.split[3]) + x.split[3].length, " do $* << #{y}")
x.insert(x.length, "end; $*")
eval(x)
$*)
else
eval(x.split[3]).to_a
end
end
então, basicamente, estamos convertendo uma string para a sintaxe ruby apropriada para loop, então podemos usar a sintaxe python em uma string para fazer:
c['for x in 1..10']
c['for x in 1..10 if x.even?']
c['x**2 for x in 1..10 if x.even?']
c['x**2 for x in 1..10']
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [2, 4, 6, 8, 10]
# [4, 16, 36, 64, 100]
# [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
ou se você não gostar da aparência da string ou de ter que usar um lambda, poderíamos abandonar a tentativa de espelhar a sintaxe python e fazer algo assim:
S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]
Ruby 2.7 introduzido, filter_map
que praticamente atinge o que você deseja (mapa + compacto):
some_array.filter_map { |x| x * 3 if x % 2 == 0 }
Você pode ler mais sobre isso aqui .
https://rubygems.org/gems/ruby_list_comprehension
plugue descarado para minha gem de Compreensão de Lista Ruby para permitir compreensões idiomáticas de lista Ruby
$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
Acho que a compreensão mais abrangente de lista seria a seguinte:
some_array.select{ |x| x * 3 if x % 2 == 0 }
Como o Ruby nos permite colocar o condicional após a expressão, obtemos uma sintaxe semelhante à versão Python da compreensão de lista. Além disso, como o select
método não inclui nada que seja igual a false
, todos os valores nulos são removidos da lista resultante e nenhuma chamada para compactar é necessária como seria o caso se tivéssemos usado map
ou em collect
vez disso.