Pemahaman daftar di Ruby

93

Untuk melakukan yang setara dengan pemahaman daftar Python, saya melakukan yang berikut:

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

Apakah ada cara yang lebih baik untuk melakukan ini ... mungkin dengan satu pemanggilan metode?

Hanya baca
sumber
3
Baik jawaban Anda dan jawaban glenn mcdonald tampaknya baik-baik saja bagi saya ... Saya tidak melihat apa yang akan Anda peroleh dengan mencoba lebih ringkas daripada keduanya.
Pistos
1
solusi ini melampaui daftar dua kali. Suntikan tidak.
Pedro Rolo
2
Beberapa jawaban yang luar biasa di sini tetapi akan sangat luar biasa juga melihat ide untuk pemahaman daftar di beberapa koleksi.
Bo Jeanes

Jawaban:

55

Jika Anda benar-benar ingin, Anda dapat membuat metode Array #rehend seperti ini:

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

Cetakan:

6
12
18

Saya mungkin hanya akan melakukannya seperti yang Anda lakukan.

Robert Gamble
sumber
2
Anda bisa menggunakan compact! untuk mengoptimalkan sedikit
Alexey
9
Ini sebenarnya tidak benar, pertimbangkan: [nil, nil, nil].comprehend {|x| x }yang kembali [].
Ted Kaplan
alexey, menurut dokumen, compact!mengembalikan nol daripada array ketika tidak ada item yang diubah, jadi saya rasa itu tidak berhasil.
Binary Phile
89

Bagaimana jika:

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

Sedikit lebih bersih, setidaknya menurut selera saya, dan menurut uji benchmark cepat sekitar 15% lebih cepat dari versi Anda ...

glenn mcdonald
sumber
4
serta some_array.map{|x| x * 3 unless x % 2}.compact, yang bisa dibilang lebih mudah dibaca / ruby-esque.
kolam renang malam
5
@nightpool unless x%2tidak berpengaruh karena 0 benar dalam ruby. Lihat: gist.github.com/jfarmer/2647362
Abhinav Srivastava
30

Saya membuat patokan cepat yang membandingkan ketiga alternatif dan map-compact tampaknya menjadi pilihan terbaik.

Uji kinerja (Rel)

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

Hasil

/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
sumber
1
Akan menarik untuk dilihat reducedi benchmark ini juga (lihat stackoverflow.com/a/17703276 ).
Adam Lindberg
3
inject==reduce
ben.snape
map_compact mungkin lebih cepat tetapi membuat array baru. inject adalah ruang yang efisien daripada map.compact dan select.map
bibstha
11

Sepertinya ada beberapa kebingungan di antara programmer Ruby di thread ini tentang pengertian list. Setiap respons tunggal mengasumsikan beberapa larik yang sudah ada sebelumnya untuk diubah. Tetapi kekuatan pemahaman daftar terletak pada larik yang dibuat dengan cepat dengan sintaks berikut:

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

Berikut ini adalah analog di Ruby (satu-satunya jawaban yang memadai di utas ini, AFAIC):

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

Dalam kasus di atas, saya membuat array bilangan bulat acak, tetapi blok itu bisa berisi apa saja. Tapi ini akan menjadi pemahaman daftar Ruby.

Menandai
sumber
1
Bagaimana Anda melakukan apa yang OP coba lakukan?
Andrew Grimm
2
Sebenarnya saya melihat sekarang OP itu sendiri memiliki beberapa daftar yang ada yang ingin diubah oleh penulis. Tetapi konsep dasar pemahaman daftar melibatkan pembuatan larik / daftar di mana sebelumnya tidak ada dengan mereferensikan beberapa iterasi. Tetapi sebenarnya, beberapa definisi formal mengatakan pemahaman daftar tidak dapat menggunakan peta sama sekali, jadi bahkan versi saya tidak halal -tetapi saya kira sedekat mungkin dengan yang bisa didapat di Ruby.
Tandai
5
Saya tidak mengerti bagaimana contoh Ruby Anda seharusnya menjadi analog dari contoh Python Anda. Kode Ruby harus membaca: squares = (0..9) .map {| x | x ** 2}
michau
4
Meskipun @michau benar, inti dari pemahaman daftar (yang diabaikan Mark) adalah bahwa pemahaman daftar itu sendiri tidak menggunakan tidak menghasilkan array - ia menggunakan generator dan rutinitas bersama untuk melakukan semua penghitungan secara streaming tanpa mengalokasikan penyimpanan sama sekali (kecuali variabel temp) hingga (iff) hasil mendarat dalam variabel array - ini adalah tujuan tanda kurung siku dalam contoh python, untuk menciutkan pemahaman ke satu set hasil. Ruby tidak memiliki fasilitas yang mirip dengan generator.
Guss
4
Oh ya, sudah (sejak 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

Saya membahas topik ini dengan Rein Henrichs, yang memberi tahu saya bahwa solusi dengan kinerja terbaik adalah

map { ... }.compact

Ini masuk akal karena menghindari pembuatan Array perantara seperti penggunaan yang tidak dapat diubah dari Enumerable#inject, dan menghindari menumbuhkan Array, yang menyebabkan alokasi. Ini umum seperti yang lainnya kecuali koleksi Anda dapat berisi elemen nihil.

Saya belum membandingkan ini dengan

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

Ada kemungkinan implementasi Ruby C Enumerable#selectjuga sangat bagus.

jvoorhis
sumber
9

Solusi alternatif yang akan bekerja di setiap implementasi dan berjalan dalam waktu O (n) bukan O (2n) adalah:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}
Pedro Rolo
sumber
11
Maksud Anda, ini melintasi daftar hanya sekali. Jika Anda menggunakan definisi formal, O (n) sama dengan O (2n). Hanya nitpicking :)
Daniel Hepper
1
@Daniel Harper :) Tidak hanya Anda yang benar, tetapi juga untuk kasus rata-rata, mengubah daftar sekali untuk membuang beberapa entri, dan sekali lagi untuk melakukan operasi dapat secara akurat lebih baik dalam kasus rata-rata :)
Pedro Rolo
Dengan kata lain, Anda melakukan 2sesuatu nkali daripada waktu 1benda ndan kemudian 1hal lain nkali :) Satu keuntungan penting dari inject/ reduceadalah mempertahankan nilnilai apa pun dalam urutan masukan yang merupakan perilaku yang lebih memahami daftar
John La Rooy
8

Saya baru saja menerbitkan permata pemahaman ke RubyGems, yang memungkinkan Anda melakukan ini:

require 'comprehend'

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

Itu ditulis dalam C; larik hanya dilintasi sekali.

histokrat
sumber
7

Enumerable memiliki grepmetode yang argumen pertamanya bisa menjadi proc predikat, dan argumen kedua opsionalnya adalah fungsi pemetaan; jadi berikut ini bekerja:

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

Ini tidak terbaca seperti beberapa saran lain (saya suka permata sederhana select.mapatau histokrat anoiaque), tetapi kekuatannya adalah bahwa itu sudah menjadi bagian dari pustaka standar, dan single-pass dan tidak melibatkan pembuatan array perantara sementara , dan tidak memerlukan nilai di luar batas seperti yang nildigunakan dalam compactsaran -menggunakan.

Peter Moulder
sumber
4

Ini lebih singkat:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}
anoiaque
sumber
2
Atau, untuk lebih banyak keangkeran tanpa poin[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]

Itu berhasil untuk saya. Itu juga bersih. Ya, itu sama dengan map, tapi menurut saya collectmembuat kodenya lebih mudah dimengerti.


select(&:even?).map()

sebenarnya terlihat lebih baik, setelah melihatnya di bawah.

Vince
sumber
2

Seperti yang disebutkan Pedro, Anda dapat menggabungkan panggilan berantai ke Enumerable#selectdan Enumerable#map, menghindari traversal pada elemen yang dipilih. Ini benar karena Enumerable#selectmerupakan spesialisasi dari lipatan atau inject. Saya memposting perkenalan yang tergesa - gesa ke topik di subreddit Ruby.

Penggabungan transformasi Array secara manual bisa membosankan, jadi mungkin seseorang bisa bermain-main dengan comprehendimplementasi Robert Gamble untuk membuat pola select/ ini maplebih cantik.

jvoorhis
sumber
2

Sesuatu seperti ini:

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

Sebut saja:

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

Yang mengembalikan:

=> [6, 12, 18]
Alexandre Magro
sumber
Apa yang salah dengan mendefinisikan lazydi Array dan kemudian:(1..6).lazy{|x|x*3 if x.even?}
Guss
1

Solusi lain tetapi mungkin bukan yang terbaik

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

atau

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

Ini adalah salah satu cara untuk mendekati ini:

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 

jadi pada dasarnya kita mengubah string menjadi sintaks ruby ​​yang tepat untuk loop maka kita dapat menggunakan sintaks python dalam string untuk melakukan:

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]

atau jika Anda tidak menyukai tampilan string atau harus menggunakan lambda, kami dapat mengabaikan upaya untuk mencerminkan sintaks python dan melakukan sesuatu seperti ini:

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

Ruby 2.7 diperkenalkan filter_mapyang cukup banyak mencapai apa yang Anda inginkan (peta + ringkas):

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

Anda dapat membaca lebih lanjut di sini .

Matheus Richard
sumber
0

https://rubygems.org/gems/ruby_list_comprehension

steker tak tahu malu untuk permata Pemahaman Daftar Ruby saya untuk memungkinkan pemahaman daftar Ruby idiomatik

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

Saya pikir pemahaman daftar paling-esque adalah sebagai berikut:

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

Karena Ruby memungkinkan kita untuk menempatkan kondisional setelah ekspresi, kita mendapatkan sintaks yang mirip dengan versi Python dari daftar pemahaman. Juga, karena selectmetode tidak menyertakan apa pun yang sama dengan false, semua nilai nihil dihapus dari daftar yang dihasilkan dan tidak diperlukan panggilan untuk memadatkan seperti yang akan terjadi jika kita telah menggunakan mapatau collectsebagai gantinya.

Christopher Roach
sumber
7
Sepertinya ini tidak berhasil. Setidaknya di Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 jika x% 2 == 0} mengevaluasi ke [2, 4, 6] Enumerable # pilih hanya peduli apakah blok mengevaluasi benar atau salah, bukan nilai apa yang dikeluarkannya, AFAIK.
Greg Campbell