Compréhension de liste en Ruby

93

Pour faire l'équivalent de la compréhension de liste Python, je fais ce qui suit:

some_array.select{|x| x % 2 == 0 }.collect{|x| x * 3}

Y a-t-il une meilleure façon de faire cela ... peut-être avec un seul appel de méthode?

Lecture seulement
la source
3
La vôtre et les réponses de Glenn McDonald me semblent bonnes ... Je ne vois pas ce que vous gagneriez en essayant d'être plus concis que les deux.
Pistos
1
cette solution traverse la liste deux fois. L'injection ne le fait pas.
Pedro Rolo
2
Quelques réponses géniales ici, mais ce serait génial aussi de voir des idées pour la compréhension de la liste dans plusieurs collections.
Bo Jeanes

Réponses:

55

Si vous le souhaitez vraiment, vous pouvez créer une méthode Array # comprehend comme celle-ci:

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

Impressions:

6
12
18

Je le ferais probablement comme vous l'avez fait.

Robert Gamble
la source
2
Vous pouvez utiliser compact! pour optimiser un peu
Alexey
9
Ce n'est pas vraiment correct, considérez: [nil, nil, nil].comprehend {|x| x }qui retourne [].
trente
alexey, selon la documentation, compact!renvoie nil au lieu du tableau lorsqu'aucun élément n'est modifié, donc je ne pense pas que cela fonctionne.
Binary Phile
89

Que diriez-vous:

some_array.map {|x| x % 2 == 0 ? x * 3 : nil}.compact

Légèrement plus propre, du moins à mon goût, et selon un rapide test de référence environ 15% plus rapide que votre version ...

glenn mcdonald
la source
4
ainsi que some_array.map{|x| x * 3 unless x % 2}.compact, qui est sans doute plus lisible / rubis-esque.
piscine nocturne
5
@nightpool unless x%2n'a aucun effet puisque 0 est la vérité en ruby. Voir: gist.github.com/jfarmer/2647362
Abhinav Srivastava
30

J'ai fait une comparaison rapide des trois alternatives et la carte compacte semble vraiment être la meilleure option.

Test de performance (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

Résultats

/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
la source
1
Il serait également intéressant de voir reducece benchmark (voir stackoverflow.com/a/17703276 ).
Adam Lindberg
3
inject==reduce
ben.snape
map_compact peut-être plus vite mais il crée un nouveau tableau. inject est peu encombrant que map.compact et select.map
bibstha
11

Il semble y avoir une certaine confusion parmi les programmeurs Ruby dans ce fil concernant ce qu'est la compréhension de liste. Chaque réponse suppose un tableau préexistant à transformer. Mais le pouvoir de la compréhension de liste réside dans un tableau créé à la volée avec la syntaxe suivante:

squares = [x**2 for x in range(10)]

Ce qui suit serait un analogue de Ruby (la seule réponse adéquate dans ce fil, AFAIC):

a = Array.new(4).map{rand(2**49..2**50)} 

Dans le cas ci-dessus, je crée un tableau d'entiers aléatoires, mais le bloc peut contenir n'importe quoi. Mais ce serait une compréhension de la liste Ruby.

marque
la source
1
Comment feriez-vous ce que le PO essaie de faire?
Andrew Grimm
2
En fait, je vois maintenant que le PO lui-même avait une liste existante que l'auteur voulait transformer. Mais la conception archétypale de la compréhension de liste implique la création d'un tableau / liste là où il n'en existait pas auparavant en référençant une itération. Mais en fait, certaines définitions formelles disent que la compréhension de liste ne peut pas du tout utiliser la carte, donc même ma version n'est pas casher - mais aussi proche que possible avec Ruby, je suppose.
Mark
5
Je ne comprends pas comment votre exemple Ruby est censé être un analogue de votre exemple Python. Le code Ruby doit lire: squares = (0..9) .map {| x | x ** 2}
michau
4
Bien que @michau ait raison, tout l'intérêt de la compréhension de liste (que Mark a négligé) est que la compréhension de liste elle-même n'utilise pas de génération de tableaux - elle utilise des générateurs et des co-routines pour faire tous les calculs en continu sans allouer du tout de stockage (sauf variables temporaires) jusqu'à ce que (ssi) les résultats atterrissent dans une variable de tableau - c'est le but des crochets dans l'exemple python, pour réduire la compréhension à un ensemble de résultats. Ruby n'a aucune installation similaire aux générateurs.
Guss
4
Oh oui, il a (depuis Ruby 2.0): squares_of_all_natural_numbers = (0..Float :: INFINITY) .lazy.map {| x | x ** 2}; p squares_of_all_natural_numbers.take (10) .to_a
michau
11

J'ai discuté de ce sujet avec Rein Henrichs, qui me dit que la solution la plus performante est

map { ... }.compact

Cela a du bon sens car cela évite de créer des tableaux intermédiaires comme avec l'utilisation immuable de Enumerable#inject, et cela évite de développer le tableau, ce qui provoque une allocation. C'est aussi général que n'importe lequel des autres, sauf si votre collection peut contenir des éléments nuls.

Je n'ai pas comparé ça avec

select {...}.map{...}

Il est possible que l'implémentation en C de Ruby Enumerable#selectsoit également très bonne.

jvoorhis
la source
9

Une solution alternative qui fonctionnera dans chaque implémentation et fonctionnera en temps O (n) au lieu de O (2n) est:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
Pedro Rolo
la source
11
Vous voulez dire qu'il parcourt la liste une seule fois. Si vous utilisez la définition formelle, O (n) est égal à O (2n). Just nitpicking :)
Daniel Hepper
1
@Daniel Harper :) Non seulement vous avez raison, mais aussi pour le cas moyen, traverser la liste une fois pour supprimer certaines entrées, puis à nouveau pour effectuer une opération peut être effectivement mieux dans les cas moyens :)
Pedro Rolo
En d' autres termes, vous faites des 2choses nfois au lieu de 1chose nfois, puis une autre 1chose nfois :) Un avantage important de inject/ reduceest qu'il conserve toutes les nilvaleurs dans la séquence d'entrée qui est plus le comportement de liste comprehensionly
John La Rooy
8

Je viens de publier le joyau de compréhension sur RubyGems, ce qui vous permet de faire ceci:

require 'comprehend'

some_array.comprehend{ |x| x * 3 if x % 2 == 0 }

C'est écrit en C; le tableau n'est parcouru qu'une seule fois.

histocrate
la source
7

Enumerable a une grepméthode dont le premier argument peut être un prédicat proc, et dont le deuxième argument facultatif est une fonction de mappage; donc ce qui suit fonctionne:

some_array.grep(proc {|x| x % 2 == 0}) {|x| x*3}

Ce n'est pas aussi lisible que quelques autres suggestions (j'aime le joyau simple select.mapou compréhensible de l'histocrate d'anoiaque), mais ses points forts sont qu'il fait déjà partie de la bibliothèque standard, et qu'il est en un seul passage et n'implique pas la création de tableaux intermédiaires temporaires , et ne nécessite pas de valeur hors limites comme celle nilutilisée dans les compactsuggestions -using.

Peter Moulder
la source
4

C'est plus concis:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
anoiaque
la source
2
Ou, pour encore plus de génialité sans point[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]

Ça marche pour moi. C'est aussi propre. Oui, c'est la même chose que map, mais je pense que cela collectrend le code plus compréhensible.


select(&:even?).map()

semble vraiment mieux, après l'avoir vu ci-dessous.

Vince
la source
2

Comme Pedro l'a mentionné, vous pouvez fusionner les appels chaînés vers Enumerable#selectet Enumerable#map, en évitant une traversée des éléments sélectionnés. Cela est vrai car il Enumerable#selects'agit d'une spécialisation de pli ou inject. J'ai posté une introduction hâtive au sujet dans le sous-répertoire Ruby.

La fusion manuelle des transformations Array peut être fastidieuse, alors peut-être que quelqu'un pourrait jouer avec l' comprehendimplémentation de Robert Gamble pour rendre ce select/ mappattern plus joli.

jvoorhis
la source
2

Quelque chose comme ça:

def lazy(collection, &blk)
   collection.map{|x| blk.call(x)}.compact
end

Appeler:

lazy (1..6){|x| x * 3 if x.even?}

Qui renvoie:

=> [6, 12, 18]
Alexandre Magro
la source
Quel est le problème avec la définition lazysur Array et ensuite:(1..6).lazy{|x|x*3 if x.even?}
Guss
1

Une autre solution mais peut-être pas la meilleure

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
la source
0

Voici une façon d'aborder ceci:

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 

Donc, fondamentalement, nous convertissons une chaîne en syntaxe ruby ​​appropriée pour la boucle, puis nous pouvons utiliser la syntaxe python dans une chaîne à faire:

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 si vous n'aimez pas l'apparence de la chaîne ou si vous devez utiliser un lambda, nous pourrions renoncer à essayer de refléter la syntaxe python et faire quelque chose comme ceci:

S = [for x in 0...9 do $* << x*2 if x.even? end, $*][1]
# [0, 4, 8, 12, 16]
Sam Michael
la source
0

Ruby 2.7 introduit filter_mapqui réalise à peu près ce que vous voulez (carte + compact):

some_array.filter_map { |x| x * 3 if x % 2 == 0 }

Vous pouvez en savoir plus ici .

Matheus Richard
la source
0

https://rubygems.org/gems/ruby_list_comprehension

plug sans vergogne pour mon joyau de compréhension de liste Ruby pour permettre la compréhension idiomatique de la liste Ruby

$l[for x in 1..10 do x + 2 end] #=> [3, 4, 5 ...]
Sam Michael
la source
-4

Je pense que la plus grande compréhension de la liste serait la suivante:

some_array.select{ |x| x * 3 if x % 2 == 0 }

Puisque Ruby nous permet de placer le conditionnel après l'expression, nous obtenons une syntaxe similaire à la version Python de la compréhension de liste. De plus, comme la selectméthode n'inclut rien qui équivaut à false, toutes les valeurs nulles sont supprimées de la liste résultante et aucun appel à compact n'est nécessaire comme ce serait le cas si nous avions utilisé mapou à la collectplace.

Christopher Roach
la source
7
Cela ne semble pas fonctionner. Au moins dans Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 si x% 2 == 0} évalue à [2, 4, 6] Enumerable # select se soucie uniquement de savoir si le bloc évalue à vrai ou faux, pas à la valeur qu'il produit, AFAIK.
Greg Campbell