Di Ruby, apakah ada metode Array yang menggabungkan 'pilih' dan 'peta'?

96

Saya memiliki array Ruby yang berisi beberapa nilai string. Aku ingin:

  1. Temukan semua elemen yang cocok dengan beberapa predikat
  2. Jalankan elemen yang cocok melalui transformasi
  3. Kembalikan hasilnya sebagai larik

Sekarang solusi saya terlihat seperti ini:

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

Apakah ada metode Array atau Enumerable yang menggabungkan select dan map menjadi satu pernyataan logis?

Seth Petry-Johnson
sumber
5
Tidak ada metode saat ini, tetapi proposal untuk menambahkannya ke Ruby: bugs.ruby-lang.org/issues/5663
stefankolb
The Enumerable#grepMetode tidak persis apa yang diminta dan telah di Ruby selama lebih dari sepuluh tahun. Dibutuhkan argumen predikat dan blok transformasi. @hirolau memberikan satu-satunya jawaban yang benar untuk pertanyaan ini.
inopinatus
2
Ruby 2.7 diperkenalkan filter_mapuntuk tujuan yang tepat ini. Info selengkapnya di sini .
SRack

Jawaban:

115

Saya biasanya menggunakan mapdan compactbersama dengan kriteria pemilihan saya sebagai postfix if. compactmenghilangkan nil.

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] 
Jed Schneider
sumber
1
Ah-ha, saya mencoba mencari cara untuk mengabaikan nol yang dikembalikan oleh blok peta saya. Terima kasih!
Seth Petry-Johnson
Tidak masalah, saya suka kompak. itu diam-diam duduk di luar sana dan melakukan tugasnya. Saya juga lebih suka metode ini daripada merangkai fungsi yang dapat dihitung untuk kriteria pemilihan sederhana karena sangat deklaratif.
Jed Schneider
4
Saya tidak yakin apakah map+ compactbenar-benar akan berkinerja lebih baik daripada injectdan memposting hasil benchmark saya ke utas terkait: stackoverflow.com/questions/310426/list-comprehension-in-ruby/…
knuton
3
ini akan menghapus semua nil, baik nil asli maupun yang tidak memenuhi kriteria Anda. Jadi hati-hati
pengguna1143669
1
Itu tidak sepenuhnya menghilangkan perangkaian mapdan select, hanya saja itu compactadalah kasus khusus rejectyang bekerja pada nils dan berkinerja agak lebih baik karena telah diterapkan langsung di C.
Joe Atzberger
53

Anda dapat menggunakan reduceuntuk ini, yang hanya membutuhkan satu izin:

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

Dengan kata lain, inisialisasi status menjadi apa yang Anda inginkan (dalam kasus kami, daftar kosong untuk diisi: [] , lalu selalu pastikan untuk mengembalikan nilai ini dengan modifikasi untuk setiap elemen dalam daftar asli (dalam kasus kami, elemen yang dimodifikasi didorong ke daftar).

Ini adalah yang paling efisien karena hanya mengulang daftar dengan satu pass ( map+ selectataucompact membutuhkan dua pass).

Dalam kasus Anda:

def example
  results = @lines.reduce([]) do |lines, line|
    lines.push( ...(line) ) if ...
    lines
  end
  return results.uniq.sort
end
Adam Lindberg
sumber
20
Bukankah each_with_objectsedikit lebih masuk akal? Anda tidak perlu mengembalikan array di akhir setiap iterasi blok. Anda bisa melakukannya my_array.each_with_object([]) { |i, a| a << i if i.condition }.
henrebotha
@henrebotha Mungkin memang begitu. Saya berasal dari latar belakang fungsional, itulah mengapa saya menemukan yang reducepertama 😊
Adam Lindberg
35

Ruby 2.7+

Ada sekarang!

Ruby 2.7 memperkenalkan filter_map untuk tujuan yang tepat ini. Ini idiomatis dan bagus, dan saya berharap itu menjadi norma segera.

Sebagai contoh:

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

Berikut bacaan yang bagus tentang masalah ini .

Semoga bermanfaat bagi seseorang!

SRack
sumber
1
Tidak peduli seberapa sering saya meningkatkan, fitur keren selalu ada di versi berikutnya.
mlt
Bagus. Salah satu masalah bisa bahwa sejak filter, selectdan find_alladalah sama, seperti mapdan collectyang, mungkin akan sulit untuk mengingat nama metode ini. Apakah itu filter_map, select_collect, find_all_mapatau filter_collect?
Eric Duminil
19

Cara lain yang berbeda untuk mendekati ini adalah menggunakan yang baru (relatif terhadap pertanyaan ini) Enumerator::Lazy:

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

The .lazyMetode mengembalikan enumerator malas. Memanggil .selectatau .mappada pencacah malas mengembalikan pencacah malas lainnya. Hanya sekali Anda menelepon .uniqapakah itu benar-benar memaksa pencacah dan mengembalikan array. Jadi yang terjadi secara efektif adalah panggilan Anda .selectdan .mapdigabungkan menjadi satu - Anda hanya mengulangi @linessekali untuk melakukan keduanya .selectdan.map .

Naluri saya adalah bahwa reducemetode Adam akan sedikit lebih cepat, tetapi saya pikir ini jauh lebih mudah dibaca.


Konsekuensi utama dari hal ini adalah tidak ada objek larik perantara yang dibuat untuk setiap panggilan metode berikutnya. Dalam @lines.select.mapsituasi normal , selectmengembalikan larik yang kemudian dimodifikasi oleh map, kembali mengembalikan larik. Sebagai perbandingan, evaluasi malas hanya membuat larik satu kali. Ini berguna ketika objek koleksi awal Anda berukuran besar. Ini juga memberdayakan Anda untuk bekerja dengan enumerator tak terbatas - mis random_number_generator.lazy.select(&:odd?).take(10).

henrebotha
sumber
4
Untuk masing-masing milik mereka. Dengan jenis solusi saya, saya dapat melihat sekilas nama metode dan segera mengetahui bahwa saya akan mengubah subset data input, membuatnya unik, dan mengurutkannya. reducesebagai "lakukan segalanya" transformasi selalu terasa cukup berantakan bagi saya.
henrebotha
2
@henrebotha: Maafkan saya jika saya salah paham tentang apa yang Anda maksud, tetapi ini adalah poin yang sangat penting: tidak benar untuk mengatakan bahwa "Anda hanya mengulangi @linessekali untuk melakukan keduanya .selectdan .map". Menggunakan .lazytidak berarti operasi operasi yang dirantai pada pencacah yang malas akan "diciutkan" menjadi satu iterasi tunggal. Ini adalah kesalahpahaman umum dari evaluasi malas operasi rantai wrt atas sebuah koleksi. (Anda dapat mengujinya dengan menambahkan putspernyataan di awal blok selectdan mappada contoh pertama. Anda akan menemukan bahwa mereka mencetak jumlah baris yang sama)
pje
1
@henrebotha: dan jika Anda menghapusnya, .lazycetakannya akan dicetak dalam jumlah yang sama. Itulah maksud saya — mapblok Anda dan blok Anda selectdieksekusi dengan jumlah yang sama di versi lazy dan eager. Versi malas tidak "menggabungkan panggilan .selectdan Anda .map"
pje
1
@pje: Efeknya lazy menggabungkan mereka karena elemen yang gagal selectkondisi tidak diteruskan ke map. Dengan kata lain: prepending lazykira-kira sama dengan mengganti selectdan mapdengan single reduce([]), dan "secara cerdas" membuat selectblok sebagai prasyarat untuk dimasukkan ke dalam reducehasil.
henrebotha
1
@henrebotha: Saya pikir itu adalah analogi yang menyesatkan untuk evaluasi malas secara umum, karena kemalasan tidak mengubah kompleksitas waktu dari algoritme ini. Inilah maksud saya: dalam setiap kasus, pilih-kemudian-peta yang malas akan selalu melakukan jumlah komputasi yang sama dengan versi yang diinginkannya. Itu tidak mempercepat apa pun, itu hanya mengubah urutan eksekusi setiap iterasi — fungsi terakhir dalam rantai "menarik" nilai-nilai yang diperlukan dari fungsi sebelumnya dalam urutan terbalik.
pje
13

Jika Anda memiliki selectyang dapat menggunakan caseoperator ( ===), grepadalah alternatif yang baik:

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"]

Jika kita membutuhkan logika yang lebih kompleks, kita dapat membuat lambda:

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***"]
hirolau
sumber
Ini bukan alternatif - ini satu-satunya jawaban yang benar untuk pertanyaan itu.
inopinatus
@inopinatus: Tidak lagi . Ini masih merupakan jawaban yang bagus. Saya tidak ingat melihat grep dengan blok sebaliknya.
Eric Duminil
8

Saya tidak yakin ada satu. The modul Enumerable , yang menambahkan selectdan map, tidak menunjukkan satu.

Anda akan diminta untuk mengirimkan dua blok ke select_and_transformmetode ini, yang akan menjadi IMHO sedikit tidak intuitif.

Jelas, Anda bisa merangkainya, yang lebih mudah dibaca:

transformed_list = lines.select{|line| ...}.map{|line| ... }
Gishu
sumber
3

Jawaban Sederhana:

Jika Anda memiliki n catatan, dan Anda ingin selectdan mapberdasarkan kondisi maka

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

Di sini, atribut adalah apa pun yang Anda inginkan dari catatan dan kondisi, Anda dapat mencentangnya.

Compact adalah untuk membilas nil yang tidak perlu yang keluar dari kondisi tersebut

Sk. Irfan
sumber
1
Anda dapat menggunakan kondisi yang sama dengan kecuali juga. Seperti yang teman saya tanyakan.
Sk. Irfan
2

Tidak, tapi Anda bisa melakukannya seperti ini:

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

Atau bahkan lebih baik:

lines.inject([]) { |all, line| all << line if check_some_property; all }
Daniel O'Hara
sumber
14
reject(&:nil?)pada dasarnya sama dengan compact.
Jörg W Mittag
Ya, jadi metode injeksi lebih baik.
Daniel O'Hara
2

Saya pikir cara ini lebih mudah dibaca, karena membagi kondisi filter dan nilai yang dipetakan sambil tetap jelas bahwa tindakannya terhubung:

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

Dan, dalam kasus spesifik Anda, hilangkan resultvariabel semuanya:

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

Di Ruby 1.9 dan 1.8.7, Anda juga dapat merangkai dan membungkus iterator hanya dengan tidak memberikan satu blok kepada mereka:

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

Tapi itu tidak benar-benar mungkin dalam kasus ini, karena tipe blok mengembalikan nilai selectdan maptidak cocok. Lebih masuk akal untuk sesuatu seperti ini:

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

AFAICS, hal terbaik yang dapat Anda lakukan adalah contoh pertama.

Inilah contoh kecilnya:

%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"]

Tapi yang benar - benar Anda inginkan adalah ["A", "B", "C", "D"].

Jörg W Mittag
sumber
Saya melakukan pencarian web yang sangat singkat tadi malam untuk "metode rantai di Ruby" dan sepertinya itu tidak didukung dengan baik. Selain itu, saya mungkin harus mencobanya ... juga, mengapa Anda mengatakan jenis argumen blok tidak cocok? Dalam contoh saya, kedua blok mengambil sebaris teks dari array saya, bukan?
Seth Petry-Johnson
@ Seth Petry-Johnson: Ya, maaf, maksud saya nilai yang dikembalikan. selectmengembalikan nilai Boolean-ish yang memutuskan apakah akan mempertahankan elemen atau tidak, mapmengembalikan nilai yang diubah. Nilai yang diubah itu sendiri mungkin akan menjadi benar, jadi semua elemen dipilih.
Jörg W Mittag
1

Anda harus mencoba menggunakan perpustakaan saya Rearmed Ruby yang telah saya tambahkan metodenya Enumerable#select_map. Berikut contohnya:

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]}
Weston Ganger
sumber
select_mapdi perpustakaan ini hanya menerapkan select { |i| ... }.map { |i| ... }strategi yang sama dari banyak jawaban di atas.
Jordan Sitkin
1

Jika Anda tidak ingin membuat dua larik yang berbeda, Anda dapat menggunakan compact!tetapi berhati-hatilah.

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

Menariknya, compact! melakukan penghapusan di tempat nihil. Nilai kembalian dari compact!array yang sama jika ada perubahan tetapi nihil jika tidak ada nil.

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

Akan menjadi satu kapal.

bibstha
sumber
0

Versi Anda:

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

Versi saya:

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

Ini akan melakukan 1 iterasi (kecuali pengurutan), dan memiliki bonus tambahan untuk menjaga keunikan (jika Anda tidak peduli tentang unik, maka buat saja hasilnya menjadi array dan results.push(line) if ...

Jordan Michael Rushing
sumber
-1

Berikut ini contohnya. Ini tidak sama dengan masalah Anda, tetapi mungkin apa yang Anda inginkan, atau dapat memberi petunjuk untuk solusi Anda:

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
zw963
sumber