ใน Ruby มีเมธอด Array ที่รวม 'select' และ 'map' หรือไม่?


97

ฉันมีอาร์เรย์ Ruby ที่มีค่าสตริงบางค่า ฉันจำเป็นต้อง:

  1. ค้นหาองค์ประกอบทั้งหมดที่ตรงกับเพรดิเคตบางส่วน
  2. เรียกใช้องค์ประกอบที่ตรงกันผ่านการเปลี่ยนแปลง
  3. ส่งคืนผลลัพธ์เป็นอาร์เรย์

ตอนนี้โซลูชันของฉันมีลักษณะดังนี้:

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

มีวิธี Array หรือ Enumerable ที่รวมการเลือกและแมปเป็นคำสั่งเชิงตรรกะเดียวหรือไม่?


5
ไม่มีวิธีการในขณะนี้ แต่มีข้อเสนอให้เพิ่มหนึ่งใน Ruby: bugs.ruby-lang.org/issues/5663
stefankolb

Enumerable#grepวิธีไม่ว่าสิ่งที่ถูกถามและได้รับในทับทิมนานกว่าสิบปี ใช้อาร์กิวเมนต์เพรดิเคตและบล็อกการเปลี่ยนแปลง @hirolau ให้คำตอบที่ถูกต้องสำหรับคำถามนี้
inopinatus

2
Ruby 2.7 กำลังเปิดตัวfilter_mapเพื่อจุดประสงค์นี้ ข้อมูลเพิ่มเติมที่นี่
SRack

คำตอบ:


116

ผมมักจะใช้mapและcompactร่วมกันพร้อมกับเกณฑ์การคัดเลือกของฉันเป็น ifpostfix compactกำจัด 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
อ๊ะฉันพยายามหาวิธีที่จะเพิกเฉยต่อสิ่งที่ไม่ได้รับกลับมาจากบล็อกแผนที่ของฉัน ขอบคุณ!
Seth Petry-Johnson

ไม่มีปัญหาฉันชอบขนาดกะทัดรัด มันนั่งอยู่ที่นั่นอย่างสงบเสงี่ยมและทำงานของมัน ฉันยังชอบวิธีนี้ในการเชื่อมโยงฟังก์ชันที่แจกแจงได้สำหรับเกณฑ์การเลือกอย่างง่ายเพราะมีการเปิดเผยมาก
Jed Schneider

4
ฉันไม่แน่ใจว่าmap+ compactจะทำงานได้ดีกว่าจริงหรือไม่injectและโพสต์ผลการวัดประสิทธิภาพของฉันไปยังเธรดที่เกี่ยวข้อง: stackoverflow.com/questions/310426/list-comp
understandion

3
สิ่งนี้จะลบ nils ทั้งหมดทั้ง nils ดั้งเดิมและ nils ที่ไม่ผ่านเกณฑ์ของคุณ ดังนั้นระวัง
user1143669

1
มันไม่ได้กำจัดการผูกมัดทั้งหมดmapและselectเป็นเพียงcompactกรณีพิเศษrejectที่ทำงานบน nils และทำงานได้ค่อนข้างดีขึ้นเนื่องจากมีการนำไปใช้โดยตรงใน C.
Joe Atzberger

54

คุณสามารถใช้reduceสำหรับสิ่งนี้ซึ่งต้องใช้เพียงใบเดียว:

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

กล่าวอีกนัยหนึ่งคือเริ่มต้นสถานะเป็นสิ่งที่คุณต้องการ (ในกรณีของเราคือรายการว่างที่จะเติม :) []จากนั้นตรวจสอบให้แน่ใจเสมอว่าได้ส่งคืนค่านี้พร้อมกับการแก้ไขสำหรับแต่ละองค์ประกอบในรายการเดิม (ในกรณีของเราคือองค์ประกอบที่แก้ไข ผลักไปที่รายการ)

วิธีนี้มีประสิทธิภาพมากที่สุดเนื่องจากจะวนซ้ำรายการด้วยบัตรเดียว ( map+ selectหรือcompactต้องใช้สองใบ)

ในกรณีของคุณ:

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

20
ไม่each_with_objectเข้าท่ากว่านี้หน่อยเหรอ? คุณไม่จำเป็นต้องส่งคืนอาร์เรย์เมื่อสิ้นสุดการทำซ้ำแต่ละครั้งของบล็อก my_array.each_with_object([]) { |i, a| a << i if i.condition }คุณก็สามารถทำได้
henrebotha

@henrebotha บางทีมันอาจจะเป็นเช่นนั้น ฉันมาจากพื้นหลังที่ใช้งานได้นั่นคือเหตุผลที่ฉันพบreduceคนแรก😊
Adam Lindberg

42

รูบี้ 2.7+

มีแล้ว!

Ruby 2.7 กำลังเปิดตัวfilter_mapเพื่อจุดประสงค์นี้ มันเป็นสำนวนและนักแสดงและฉันคาดว่ามันจะกลายเป็นบรรทัดฐานในไม่ช้า

ตัวอย่างเช่น:

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

นี่คือการอ่านที่ดีในเรื่องนี้

หวังว่าจะมีประโยชน์กับใครบางคน!


1
ไม่ว่าฉันจะอัปเกรดบ่อยแค่ไหนฟีเจอร์เด็ดก็ยังอยู่ในเวอร์ชันถัดไปเสมอ
mlt

ดี. ปัญหาหนึ่งที่อาจเป็นได้ว่าตั้งแต่filter, selectและfind_allมีความหมายเหมือนเช่นเดียวกับที่mapและcollectมีมันอาจจะยากที่จะจำชื่อของวิธีการนี้ มันfilter_map, select_collect, find_all_mapหรือfilter_collect?
Eric Duminil

19

อีกวิธีหนึ่งในการเข้าถึงสิ่งนี้คือการใช้ใหม่ (เทียบกับคำถามนี้) Enumerator::Lazy:

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

.lazyวิธีการส่งกลับแจงนับขี้เกียจ การโทร.selectหรือ.mapผู้แจงนับที่ขี้เกียจจะส่งกลับตัวแจงอีกตัวที่ขี้เกียจ เพียงครั้งเดียวที่คุณเรียก.uniqมันจะบังคับตัวแจงนับและส่งคืนอาร์เรย์ ดังนั้นสิ่งที่เกิดขึ้นเป็นอย่างมีประสิทธิภาพของคุณ.selectและ.mapสายจะรวมกันเป็นหนึ่ง - คุณจะย้ำกว่า@linesครั้งที่จะทำทั้งสองอย่างและ.select.map

สัญชาตญาณของฉันคือreduceวิธีการของอดัมจะเร็วขึ้นเล็กน้อย แต่ฉันคิดว่านี่อ่านได้ไกลกว่า


ผลลัพธ์หลักของสิ่งนี้คือไม่มีการสร้างอ็อบเจ็กต์อาร์เรย์กลางสำหรับการเรียกเมธอดแต่ละครั้งที่ตามมา ใน@lines.select.mapสถานการณ์ปกติselectจะส่งคืนอาร์เรย์ที่ถูกแก้ไขโดยmapส่งคืนอาร์เรย์อีกครั้ง เมื่อเปรียบเทียบแล้วการประเมินแบบขี้เกียจจะสร้างอาร์เรย์เพียงครั้งเดียว สิ่งนี้มีประโยชน์เมื่อวัตถุคอลเลกชันเริ่มต้นของคุณมีขนาดใหญ่ นอกจากนี้ยังช่วยให้คุณสามารถทำงานร่วมกับ enumerators อนันต์ - random_number_generator.lazy.select(&:odd?).take(10)เช่น


4
ให้กับแต่ละคนเอง ด้วยวิธีการแก้ปัญหาของฉันฉันสามารถดูชื่อเมธอดและรู้ได้ทันทีว่าฉันกำลังจะแปลงชุดย่อยของข้อมูลอินพุตทำให้ไม่ซ้ำกันและจัดเรียง reduceการเปลี่ยนแปลง "ทำทุกอย่าง" มักจะทำให้ฉันรู้สึกยุ่ง
henrebotha

2
@henrebotha: ยกโทษให้ฉันถ้าฉันเข้าใจผิดในสิ่งที่คุณหมายถึง แต่นี่เป็นประเด็นสำคัญมาก: การพูดว่า "คุณทำซ้ำเพียงครั้งเดียวเพื่อทำทั้งสองอย่างและ" ไม่ถูกต้อง การใช้ไม่ได้หมายความว่าการดำเนินการที่ถูกล่ามโซ่บนตัวแจงนับที่เกียจคร้านจะทำให้ "ยุบ" เป็นการวนซ้ำเพียงครั้งเดียว นี่เป็นความเข้าใจผิดที่พบบ่อยเกี่ยวกับการดำเนินการผูกมัด WRT การประเมินผลแบบเกียจคร้านเหนือคอลเล็กชัน (คุณสามารถทดสอบสิ่งนี้ได้โดยเพิ่มคำสั่งที่จุดเริ่มต้นของและบล็อกในตัวอย่างแรกคุณจะพบว่าพวกเขาพิมพ์จำนวนบรรทัดเท่ากัน)@lines.select.map.lazyputsselectmap
pje

1
@henrebotha: และถ้าคุณลบ.lazyมันจะพิมพ์จำนวนครั้งเดียวกัน นั่นคือประเด็นของฉัน - mapบล็อกของคุณและบล็อกของคุณselectถูกดำเนินการในจำนวนครั้งเท่ากันในเวอร์ชันขี้เกียจและกระตือรือร้น รุ่นขี้เกียจไม่ "รวมของคุณ.selectและ.mapโทร"
pje

1
@pje: ผล lazyรวมเข้าด้วยกันเนื่องจากองค์ประกอบที่ล้มเหลวselectเงื่อนไขจะไม่ถูกส่งต่อไปยังไฟล์map. กล่าวอีกนัยหนึ่ง: การเติมเงินล่วงหน้าlazyจะเทียบเท่ากับการแทนที่selectและmapด้วยตัวเดียวreduce([])และ "อย่างชาญฉลาด" ทำให้selectบล็อกเป็นเงื่อนไขเบื้องต้นในการรวมไว้ในreduceผลลัพธ์ของผลลัพธ์
henrebotha

1
@henrebotha: ฉันคิดว่าเป็นการเปรียบเทียบที่ทำให้เข้าใจผิดสำหรับการประเมินแบบขี้เกียจโดยทั่วไปเพราะความเกียจคร้านไม่ได้เปลี่ยนความซับซ้อนของเวลาของอัลกอริทึมนี้ นี่คือประเด็นของฉัน: ในทุกกรณีการเลือกแล้วแผนที่ที่ขี้เกียจมักจะทำการคำนวณจำนวนเท่ากันกับเวอร์ชันที่ต้องการ มันไม่ได้เร่งความเร็วอะไรเลยเพียงแค่เปลี่ยนลำดับการดำเนินการของการวนซ้ำแต่ละครั้งซึ่งเป็นฟังก์ชันสุดท้ายในห่วงโซ่ "ดึง" ค่าตามความจำเป็นจากฟังก์ชันก่อนหน้าในลำดับย้อนกลับ
pje

13

หากคุณมีselectที่สามารถใช้caseโอเปอเรเตอร์ ( ===) grepเป็นทางเลือกที่ดี:

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

หากเราต้องการตรรกะที่ซับซ้อนมากขึ้นเราสามารถสร้าง 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***"]

ไม่ใช่ทางเลือก - เป็นคำตอบเดียวที่ถูกต้องสำหรับคำถามนี้
inopinatus

@inopinatus: ไม่อีกแล้ว นี่ยังคงเป็นคำตอบที่ดีแม้ว่า ฉันจำไม่ได้ว่าเห็น grep กับบล็อกเป็นอย่างอื่น
Eric Duminil

9

ฉันไม่แน่ใจว่ามีอยู่ โมดูล Enumerableซึ่งจะเพิ่มselectและmapไม่ได้แสดงให้เห็นอย่างใดอย่างหนึ่ง

คุณจะต้องผ่านสองช่วงตึกไปยังselect_and_transformเมธอดซึ่งอาจจะเป็น IMHO ที่ไม่เข้าใจง่ายสักหน่อย

เห็นได้ชัดว่าคุณสามารถเชื่อมโยงเข้าด้วยกันซึ่งสามารถอ่านได้มากขึ้น:

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

3

คำตอบง่ายๆ:

หากคุณมี n ระเบียนและคุณต้องการselectและmapขึ้นอยู่กับเงื่อนไขแล้ว

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

ที่นี่แอตทริบิวต์คือสิ่งที่คุณต้องการจากบันทึกและเงื่อนไขที่คุณสามารถใส่เช็คได้

กะทัดรัดคือการล้าง nil ที่ไม่จำเป็นซึ่งออกมาจากเงื่อนไข if นั้น


1
คุณสามารถใช้สิ่งเดียวกันกับยกเว้นเงื่อนไขได้เช่นกัน ตามที่เพื่อนถาม.
สก. Irfan

2

ไม่ แต่คุณสามารถทำได้ดังนี้:

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

หรือดีกว่า:

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

14
reject(&:nil?)compactเป็นพื้นเดียวกับ
Jörg W Mittag

ใช่ดังนั้นวิธีการฉีดจะดียิ่งขึ้น
Daniel O'Hara

2

ฉันคิดว่าวิธีนี้อ่านได้ง่ายกว่าเพราะแยกเงื่อนไขตัวกรองและค่าที่แมปในขณะที่ยังคงชัดเจนว่าการกระทำนั้นเชื่อมต่อกัน:

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

และในกรณีเฉพาะของคุณให้กำจัดresultตัวแปรทั้งหมดเข้าด้วยกัน:

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

ใน Ruby 1.9 และ 1.8.7 คุณสามารถเชื่อมโยงและพันตัวทำซ้ำได้โดยไม่ต้องส่งผ่านบล็อกไปให้

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

แต่มันเป็นไปไม่ได้จริง ๆ ในกรณีนี้เนื่องจากประเภทของค่าที่ส่งคืนบล็อกของselectและmapไม่ตรงกัน มันสมเหตุสมผลกว่าสำหรับสิ่งนี้:

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

AFAICS สิ่งที่ดีที่สุดที่คุณทำได้คือตัวอย่างแรก

นี่คือตัวอย่างเล็ก ๆ :

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

แต่สิ่งที่คุณจริงๆ["A", "B", "C", "D"]ต้องการคือ


เมื่อคืนฉันทำการค้นหาเว็บสั้น ๆ เกี่ยวกับ "method chaining in Ruby" และดูเหมือนว่าจะไม่ได้รับการสนับสนุน โธ่ฉันน่าจะลองแล้ว ... แล้วทำไมคุณถึงบอกว่าประเภทของอาร์กิวเมนต์บล็อกไม่ตรงกัน? ในตัวอย่างของฉันทั้งสองบล็อกกำลังรับบรรทัดข้อความจากอาร์เรย์ของฉันใช่ไหม
Seth Petry-Johnson

@Seth Petry-Johnson: ใช่ขอโทษฉันหมายถึงค่าที่ส่งคืน selectส่งคืนค่าบูลีน - ish ที่ตัดสินใจว่าจะเก็บองค์ประกอบไว้หรือไม่mapส่งคืนค่าที่แปลงแล้ว มูลค่าที่เปลี่ยนไปนั้นน่าจะเป็นจริงดังนั้นจึงมีการเลือกองค์ประกอบทั้งหมด
Jörg W Mittag

1

คุณควรลองใช้ห้องสมุดของฉันRearmed ทับทิมEnumerable#select_mapที่ฉันได้เพิ่มวิธีการ นี่คือตัวอย่าง:

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_mapในไลบรารีนี้ใช้select { |i| ... }.map { |i| ... }กลยุทธ์เดียวกันจากคำตอบมากมายข้างต้น
Jordan Sitkin

1

หากคุณไม่ต้องการสร้างอาร์เรย์ที่แตกต่างกันสองอาร์เรย์คุณสามารถใช้ได้compact!แต่ระวังให้ดี

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

ที่น่าสนใจคือcompact!การกำจัดศูนย์ออก ค่าที่ส่งคืนของcompact!เป็นอาร์เรย์เดียวกันหากมีการเปลี่ยนแปลง แต่ไม่มีศูนย์ถ้าไม่มีศูนย์

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

จะเป็นซับเดียว.


0

เวอร์ชันของคุณ:

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

เวอร์ชันของฉัน:

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

สิ่งนี้จะทำการวนซ้ำ 1 ครั้ง (ยกเว้นการเรียงลำดับ) และมีโบนัสเพิ่มเติมในการรักษาความเป็นเอกลักษณ์ (ถ้าคุณไม่สนใจ uniq ให้สร้างผลลัพธ์เป็นอาร์เรย์และ results.push(line) if ...


-1

นี่คือตัวอย่าง ไม่เหมือนกับปัญหาของคุณ แต่อาจเป็นสิ่งที่คุณต้องการหรือสามารถให้เบาะแสในการแก้ปัญหาของคุณ:

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.