Em Ruby, existe um método Array que combina 'select' e 'map'?


95

Eu tenho uma matriz Ruby contendo alguns valores de string. Eu preciso:

  1. Encontre todos os elementos que correspondem a algum predicado
  2. Execute os elementos correspondentes por meio de uma transformação
  3. Retorna os resultados como uma matriz

No momento, minha solução é assim:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Existe um método Array ou Enumerable que combina selecionar e mapear em uma única instrução lógica?


5
Não há método no momento, mas uma proposta para adicionar um ao Ruby: bugs.ruby-lang.org/issues/5663
stefankolb

O Enumerable#grepmétodo faz exatamente o que foi pedido e está em Ruby há mais de dez anos. Leva um argumento de predicado e um bloco de transformação. @hirolau dá a única resposta correta para esta pergunta.
inopinatus

2
Ruby 2.7 está apresentando filter_mapexatamente para esse propósito. Mais informações aqui .
SRack de

Respostas:


114

Eu geralmente uso mape compactjunto com meus critérios de seleção como um postfix if. compactse livrar dos nils.

jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}    
 => [3, 3, 3, nil, nil, nil] 


jruby-1.5.0 > [1,1,1,2,3,4].map{|n| n*3 if n==1}.compact
 => [3, 3, 3] 

1
Ah-ha, eu estava tentando descobrir como ignorar os nils retornados pelo meu bloco de mapa. Obrigado!
Seth Petry-Johnson

Sem problemas, eu adoro compactos. ele discretamente senta lá fora e faz seu trabalho. Eu também prefiro esse método a encadear funções enumeráveis ​​para critérios de seleção simples porque é muito declarativo.
Jed Schneider

4
Eu não tinha certeza se map+ compactteria realmente um desempenho melhor do que injecte postei meus resultados de benchmark em um tópico relacionado: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton

3
isso removerá todos os nils, tanto os nils originais quanto aqueles que falham em seus critérios. Portanto, cuidado
user1143669

1
Isso não elimina totalmente o encadeamento mape select, é apenas compactum caso especial rejectque funciona em nils e tem um desempenho um pouco melhor por ter sido implementado diretamente em C.
Joe Atzberger

53

Você pode usar reducepara isso, o que requer apenas uma passagem:

[1,1,1,2,3,4].reduce([]) { |a, n| a.push(n*3) if n==1; a }
=> [3, 3, 3] 

Em outras palavras, inicialize o estado para ser o que você deseja (no nosso caso, uma lista vazia para preencher: [] , então sempre certifique-se de retornar este valor com modificações para cada elemento na lista original (no nosso caso, o elemento modificado empurrado para a lista).

Este é o mais eficiente uma vez que só faz um loop na lista com uma passagem ( map+ selectoucompact requer duas passagens).

No seu caso:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end

20
Não each_with_objectfaz um pouco mais de sentido? Você não precisa retornar o array no final de cada iteração do bloco. Você pode simplesmente fazer my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha

@henrebotha Talvez sim. Estou vindo de um background funcional, é por isso que descobri reduceprimeiro 😊
Adam Lindberg

33

Ruby 2.7+

Existe agora!

Ruby 2.7 está apresentando filter_mapexatamente para esse propósito. É idiomático e de alto desempenho, e espero que se torne a norma em breve.

Por exemplo:

numbers = [1, 2, 5, 8, 10, 13]
enum.filter_map { |i| i * 2 if i.even? }
# => [4, 16, 20]

Aqui está uma boa leitura sobre o assunto .

Espero que seja útil para alguém!


1
Não importa com que frequência eu atualize, um recurso interessante está sempre na próxima versão.
mlt

Agradável. Um problema pode ser que filter, uma vez que selecte find_allsão sinônimos, assim como mape collectsão, pode ser difícil lembrar o nome desse método. É filter_map, select_collect, find_all_mapou filter_collect?
Eric Duminil

19

Outra maneira diferente de abordar isso é usando o novo (em relação a esta questão) Enumerator::Lazy:

def example
  @lines.lazy
        .select { |line| line.property == requirement }
        .map    { |line| transforming_method(line) }
        .uniq
        .sort
end

O .lazymétodo retorna um enumerador preguiçoso. Chamar .selectou .mapem um enumerador preguiçoso retorna outro enumerador preguiçoso. Somente quando você chama .uniq, ele realmente força o enumerador e retorna um array. Então, o que efetivamente acontece é a sua .selecte .mapas chamadas são combinados em um único - você só iterar @linesuma vez para fazer as duas coisas .selecte.map .

Meu instinto é que o reducemétodo de Adam será um pouco mais rápido, mas acho que isso é muito mais legível.


A principal consequência disso é que nenhum objeto de array intermediário é criado para cada chamada de método subsequente. Em uma @lines.select.mapsituação normal , selectretorna uma matriz que é então modificada por map, novamente retornando uma matriz. Em comparação, a avaliação preguiçosa cria um array apenas uma vez. Isso é útil quando o objeto da coleção inicial é grande. Ele também permite que você trabalhe com recenseadores infinitas - eg random_number_generator.lazy.select(&:odd?).take(10).


4
Cada um com sua mania. Com meu tipo de solução, posso dar uma olhada nos nomes dos métodos e saber imediatamente que vou transformar um subconjunto dos dados de entrada, torná-lo único e classificá-lo. reducecomo uma transformação do tipo "fazer tudo", sempre me parece bastante complicado.
henrebotha

2
@henrebotha: Perdoe-me se entendi mal o que você quis dizer, mas este é um ponto muito importante: não é correto dizer que "você apenas itera @linesuma vez para fazer ambos .selecte .map". Usar .lazynão significa que operações de operações encadeadas em um enumerador preguiçoso ficarão "reduzidas" em uma única iteração. Este é um mal-entendido comum de avaliação preguiçosa em operações de encadeamento de uma coleção. (Você pode testar isso adicionando uma putsinstrução ao início dos blocos selecte mapno primeiro exemplo. Você verá que eles imprimem o mesmo número de linhas)
pje

1
@henrebotha: e se você remover o .lazyele imprime o mesmo número de vezes. Esse é o meu ponto - seu mapbloco e seu selectbloco são executados o mesmo número de vezes nas versões lazy e eager. A versão preguiçosa não "combina seu .selecte .mapchama"
pje

1
@pje: Na verdade os lazy combina porque um elemento que falha na selectcondição não é passado para o map. Em outras palavras: prefixar lazyé aproximadamente equivalente a substituir selecte mapcom um único reduce([]), e "inteligentemente" tornar selecto bloco de uma pré-condição para inclusão no reduceresultado de.
henrebotha

1
@henrebotha: Acho que é uma analogia enganosa para avaliação preguiçosa em geral, porque preguiça não altera a complexidade de tempo desse algoritmo. Este é o meu ponto: em todos os casos, um preguiçoso select-then-map sempre realizará o mesmo número de cálculos que sua versão inicial. Isso não acelera nada, apenas altera a ordem de execução de cada iteração - a última função da cadeia "puxa" valores conforme necessário das funções anteriores na ordem reversa.
pje

13

Se você tem um selectque pode usar o caseoperador ( ===), grepé uma boa alternativa:

p [1,2,'not_a_number',3].grep(Integer){|x| -x } #=> [-1, -2, -3]

p ['1','2','not_a_number','3'].grep(/\D/, &:upcase) #=> ["NOT_A_NUMBER"]

Se precisarmos de uma lógica mais complexa, podemos criar lambdas:

my_favourite_numbers = [1,4,6]

is_a_favourite_number = -> x { my_favourite_numbers.include? x }

make_awesome = -> x { "***#{x}***" }

my_data = [1,2,3,4]

p my_data.grep(is_a_favourite_number, &make_awesome) #=> ["***1***", "***4***"]

Não é uma alternativa - é a única resposta correta para a pergunta.
inopinatus

@inopinatus: Não mais . Essa ainda é uma boa resposta, no entanto. Não me lembro de ter visto grep com um bloco de outra forma.
Eric Duminil

8

Não tenho certeza se há um. O módulo Enumerable , que adiciona selecte map, não mostra um.

Você teria que passar dois blocos para o select_and_transformmétodo, o que seria um IMHO um pouco não intuitivo.

Obviamente, você pode apenas encadea-los, o que é mais legível:

transformed_list = lines.select{|line| ...}.map{|line| ... }

3

Resposta simples:

Se você tiver n registros e quiser selecte com mapbase na condição, então

records.map { |record| record.attribute if condition }.compact

Aqui, o atributo é o que você quiser do registro e a condição você pode colocar qualquer verificação.

compacto é limpar os nulos desnecessários que saíram dessa condição se


1
Você pode usar o mesmo com a condição menos também. Como meu amigo perguntou.
Sk. Irfan

2

Não, mas você pode fazer assim:

lines.map { |line| do_some_action if check_some_property  }.reject(&:nil?)

Ou melhor ainda:

lines.inject([]) { |all, line| all << line if check_some_property; all }

14
reject(&:nil?)é basicamente o mesmo que compact.
Jörg W Mittag

Sim, então o método de injeção é ainda melhor.
Daniel O'Hara

2

Acho que dessa forma é mais legível, porque divide as condições do filtro e o valor mapeado, mantendo claro que as ações estão conectadas:

results = @lines.select { |line|
  line.should_include?
}.map do |line|
  line.value_to_map
end

E, no seu caso específico, elimine a resultvariável completamente:

def example
  @lines.select { |line|
    line.should_include?
  }.map { |line|
    line.value_to_map
  }.uniq.sort
end

1
def example
  @lines.select {|line| ... }.map {|line| ... }.uniq.sort
end

No Ruby 1.9 e 1.8.7, você também pode encadear e agrupar iteradores simplesmente não passando um bloco para eles:

enum.select.map {|bla| ... }

Mas não é realmente possível neste caso, uma vez que os tipos de bloco retornam valores de selecte mapnão coincidem. Faz mais sentido para algo assim:

enum.inject.with_index {|(acc, el), idx| ... }

AFAICS, o melhor que você pode fazer é o primeiro exemplo.

Aqui está um pequeno exemplo:

%w[a b 1 2 c d].map.select {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["a", "b", "c", "d"]

%w[a b 1 2 c d].select.map {|e| if /[0-9]/ =~ e then false else e.upcase end }
# => ["A", "B", false, false, "C", "D"]

Mas o que você realmente quer é ["A", "B", "C", "D"].


Eu fiz uma breve pesquisa na web ontem à noite por "encadeamento de métodos em Ruby" e parecia que não era bem suportado. Sim, eu provavelmente deveria ter tentado ... também, por que você diz que os tipos de argumentos de bloco não correspondem? No meu exemplo, os dois blocos estão pegando uma linha de texto do meu array, certo?
Seth Petry-Johnson

@Seth Petry-Johnson: Sim, desculpe, eu quis dizer os valores de retorno. selectretorna um valor booleano que decide se mantém o elemento ou não, mapretorna o valor transformado. O valor transformado em si provavelmente será verdadeiro, portanto, todos os elementos serão selecionados.
Jörg W Mittag

1

Você deve tentar usar minha biblioteca Rearmed Ruby na qual adicionei o método Enumerable#select_map. Aqui está um exemplo:

items = [{version: "1.1"}, {version: nil}, {version: false}]

items.select_map{|x| x[:version]} #=> [{version: "1.1"}]
# or without enumerable monkey patch
Rearmed.select_map(items){|x| x[:version]}

select_mapnesta biblioteca apenas implementa a mesma select { |i| ... }.map { |i| ... }estratégia de muitas respostas acima.
Jordan Sitkin

1

Se você não quiser criar dois arrays diferentes, pode usar, compact!mas tenha cuidado com isso.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}
new_array.compact!

Curiosamente, compact!faz uma remoção de nada no local. O valor de retorno de compact!é o mesmo array se houver alterações, mas nulo se não houver nulos.

array = [1,1,1,2,3,4]
new_array = map{|n| n*3 if n==1}.tap { |array| array.compact! }

Seria um forro.


0

Sua versão:

def example
  matchingLines = @lines.select{ |line| ... }
  results = matchingLines.map{ |line| ... }
  return results.uniq.sort
end

Minha versão:

def example
  results = {}
  @lines.each{ |line| results[line] = true if ... }
  return results.keys.sort
end

Isso fará 1 iteração (exceto a classificação) e tem o bônus adicional de manter a exclusividade (se você não se importa com o uniq, então apenas faça dos resultados uma matriz e results.push(line) if ...


-1

Aqui está um exemplo. Não é o mesmo que o seu problema, mas pode ser o que você deseja, ou pode dar uma pista para sua solução:

def example
  lines.each do |x|
    new_value = do_transform(x)
    if new_value == some_thing
      return new_value    # here jump out example method directly.
    else
      next                # continue next iterate.
    end
  end
end
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.