Compreensão de lista em Ruby

93

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?

Somente leitura
fonte
3
As suas respostas e as de glenn mcdonald parecem boas para mim ... Não vejo o que você ganharia tentando ser mais conciso do que qualquer um deles.
Pistos
1
esta solução atravessa a lista duas vezes. A injeção não.
Pedro Rolo
2
Algumas respostas incríveis aqui, mas seria incrível ver ideias para a compreensão de listas em várias coleções.
Bo Jeanes

Respostas:

55

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.

Robert Gamble
fonte
2
Você pode usar o compacto! para otimizar um pouco
Alexey
9
Isso não é realmente correto, considere: o [nil, nil, nil].comprehend {|x| x }que retorna [].
trinta
alexey, de acordo com os documentos, compact!retorna nil em vez da matriz quando nenhum item é alterado, então não acho que funcione.
Binário Phile
89

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

glenn mcdonald
fonte
4
bem como some_array.map{|x| x * 3 unless x % 2}.compact, que é indiscutivelmente mais legível / ruby-esque.
Nightpool
5
@nightpool unless x%2não tem efeito porque 0 é verdadeiro em rubi. Consulte: gist.github.com/jfarmer/2647362
Abhinav Srivastava
30

Fiz um rápido benchmark comparando as três alternativas e o map-compact realmente parece ser a melhor opção.

Teste de desempenho (Rails)

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

Resultados

/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
Knuton
fonte
1
Seria interessante ver reduceneste benchmark também (consulte stackoverflow.com/a/17703276 ).
Adam Lindberg de
3
inject==reduce
ben.snape
map_compact talvez mais rápido, mas está criando uma nova matriz. inject é eficiente em termos de espaço do que map.compact e select.map
bibstha 01 de
11

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.

Marca
fonte
1
Como você faria o que o OP está tentando fazer?
Andrew Grimm
2
Na verdade, vejo agora que o próprio OP tinha alguma lista existente que o autor queria transformar. Mas a concepção arquetípica de compreensão de lista envolve a criação de um array / lista onde não existia antes, referenciando alguma iteração. Mas, na verdade, algumas definições formais dizem que a compreensão de lista não pode usar mapa, então mesmo minha versão não é kosher - mas o mais próximo que alguém poderia chegar em Ruby, eu acho.
Marcos
5
Eu não entendo como seu exemplo Ruby deve ser análogo ao seu exemplo Python. O código Ruby deve ser: quadrados = (0..9) .map {| x | x ** 2}
michau
4
Embora @michau esteja certo, todo o ponto de compreensão de lista (que Mark negligenciou) é que a compreensão de lista em si não usa arrays de não - ela usa geradores e co-rotinas para fazer todos os cálculos em um fluxo contínuo sem alocar armazenamento (exceto variáveis ​​temporárias) até (iff) os resultados chegarem a uma variável de array - este é o propósito dos colchetes no exemplo python, para reduzir a compreensão a um conjunto de resultados. Ruby não possui recursos semelhantes aos geradores.
Guss
4
Sim, tem (desde o Ruby 2.0): squares_of_all_natural_numbers = (0..Float :: INFINITY) .lazy.map {| x | x ** 2}; p square_of_all_natural_numbers.take (10) .to_a
michau
11

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#selecttambém seja muito boa.

jvoorhis
fonte
9

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}
Pedro Rolo
fonte
11
Você quer dizer que percorre a lista apenas uma vez. Se você seguir a definição formal, O (n) é igual a O (2n). Apenas criticando :)
Daniel Hepper
1
@Daniel Harper :) Não só tens razão, mas também para o caso médio, atravessar a lista uma vez para descartar algumas entradas, e depois para realizar uma operação pode ser realmente melhor na média dos casos :)
Pedro Rolo
Em outras palavras, você está fazendo 2coisas nvezes em vez de 1coisas nvezes e depois outra 1coisa nvezes :) Uma vantagem importante de inject/ reduceé que ele preserva quaisquer nilvalores na sequência de entrada, que é um comportamento mais abrangente de lista
John La Rooy
8

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.

histocrata
fonte
7

Enumerable tem um grepmé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.mapanoiaque 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 nilusado nas compactsugestões -using.

Peter Moulder
fonte
4

Isso é mais conciso:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
anoiaque
fonte
2
Ou, para ainda mais maravilhas sem pontos[1,2,3,4,5,6].select(&:even?).map(&3.method(:*))
Jörg W Mittag
4
[1, 2, 3, 4, 5, 6].collect{|x| x * 3 if x % 2 == 0}.compact
=> [6, 12, 18]

Isso funciona para mim. Também está limpo. Sim, é o mesmo que map, mas acho que collecttorna o código mais compreensível.


select(&:even?).map()

na verdade, parece melhor, depois de ver abaixo.

Vince
fonte
2

Como o Pedro mencionou, você pode fundir as chamadas encadeadas para Enumerable#selecte 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 comprehendimplementação de Robert Gamble para tornar este select/ mappadrão mais bonito.

jvoorhis
fonte
2

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]
Alexandre Magro
fonte
O que há de errado em definir lazyem Array e então:(1..6).lazy{|x|x*3 if x.even?}
Guss
1

Outra solução, mas talvez não a melhor

some_array.flat_map {|x| x % 2 == 0 ? [x * 3] : [] }

ou

some_array.each_with_object([]) {|x, list| x % 2 == 0 ? list.push(x * 3) : nil }
joegiralt
fonte
0

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]
Sam Michael
fonte
0

Ruby 2.7 introduzido, filter_mapque 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 .

Matheus Richard
fonte
0

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 ...]
Sam Michael
fonte
-4

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 selectmé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 mapou em collectvez disso.

Christopher Roach
fonte
7
Isso não parece funcionar. Pelo menos em Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 se x% 2 == 0} é avaliado como [2, 4, 6] Enumerable # select apenas se preocupa se o bloco é avaliado como verdadeiro ou falso, não com qual valor ele produz, AFAIK.
Greg Campbell