เขียนรายการความเข้าใจใน Ruby


93

เพื่อให้เทียบเท่ากับความเข้าใจในรายการ Python ฉันกำลังทำสิ่งต่อไปนี้:

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

มีวิธีที่ดีกว่าในการทำเช่นนี้หรือไม่ ...


3
ทั้งคำตอบของคุณและเกลนน์แมคโดนัลด์ดูเหมือนจะดีสำหรับฉัน ... ฉันไม่เห็นว่าคุณจะได้อะไรจากการพยายามให้รัดกุมกว่านี้
Pistos

1
โซลูชันนี้จะข้ามรายการสองครั้ง การฉีดไม่ได้
Pedro Rolo

2
คำตอบที่ยอดเยี่ยมบางส่วนที่นี่ แต่มันก็ยอดเยี่ยมเช่นกันหากเห็นแนวคิดในการทำความเข้าใจรายการในคอลเลกชันต่างๆ
Bo Jeanes

คำตอบ:


55

หากคุณต้องการจริงๆคุณสามารถสร้าง Array # comprehend method ดังนี้:

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

พิมพ์:

6
12
18

ฉันอาจจะทำแบบที่คุณทำ


2
คุณสามารถใช้ขนาดกะทัดรัด! เพื่อเพิ่มประสิทธิภาพเล็กน้อย
Alexey

9
นี้ไม่ได้เป็นจริงที่ถูกต้องพิจารณา: ซึ่งผลตอบแทน[nil, nil, nil].comprehend {|x| x } []
Ted Kaplan

alexey ตามเอกสารcompact!ส่งคืนค่า nil แทนอาร์เรย์เมื่อไม่มีการเปลี่ยนแปลงรายการดังนั้นฉันไม่คิดว่าจะได้ผล
Binary Phile

89

วิธีการแข่งขัน:

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

สะอาดกว่าเล็กน้อยอย่างน้อยก็ตามรสนิยมของฉันและจากการทดสอบเกณฑ์มาตรฐานอย่างรวดเร็วเร็วกว่ารุ่นของคุณประมาณ 15% ...


4
เช่นเดียวกับsome_array.map{|x| x * 3 unless x % 2}.compactซึ่งเป็นเนื้อหาที่อ่านได้ง่ายกว่า / ruby-esque
nightpool

5
@nightpool unless x%2ไม่มีผลเนื่องจาก 0 เป็นความจริงในทับทิม ดู: gist.github.com/jfarmer/2647362
Abhinav Srivastava

30

ฉันสร้างเกณฑ์มาตรฐานอย่างรวดเร็วโดยเปรียบเทียบทางเลือกทั้งสามและขนาดกะทัดรัดของแผนที่ดูเหมือนจะเป็นตัวเลือกที่ดีที่สุด

การทดสอบประสิทธิภาพ (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

ผล

/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

1
ก็น่าสนใจที่จะเห็นreduceในเกณฑ์มาตรฐานนี้เช่นกัน (ดูstackoverflow.com/a/17703276 )
Adam Lindberg

3
inject==reduce
ben.snape

map_compact อาจเร็วกว่า แต่กำลังสร้างอาร์เรย์ใหม่ ฉีดเป็นพื้นที่ที่มีประสิทธิภาพกว่า map.compact และ select.map
bibstha

11

ดูเหมือนว่าจะมีความสับสนระหว่างโปรแกรมเมอร์ Ruby ในหัวข้อนี้เกี่ยวกับความเข้าใจในรายการ ทุกการตอบกลับจะถือว่าอาร์เรย์ที่มีอยู่ก่อนหน้าบางส่วนที่จะแปลง แต่พลังของความเข้าใจในรายการอยู่ในอาร์เรย์ที่สร้างขึ้นได้ทันทีด้วยไวยากรณ์ต่อไปนี้:

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

ต่อไปนี้จะเป็นอะนาล็อกใน Ruby (คำตอบเดียวที่เพียงพอในเธรดนี้ AFAIC):

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

ในกรณีข้างต้นฉันกำลังสร้างอาร์เรย์ของจำนวนเต็มแบบสุ่ม แต่บล็อกอาจมีอะไรก็ได้ แต่นี่จะเป็นการเข้าใจรายชื่อ Ruby


1
คุณจะทำสิ่งที่ OP พยายามทำอย่างไร?
Andrew Grimm

2
อันที่จริงฉันเห็นว่าตอนนี้ OP เองก็มีรายชื่อที่ผู้เขียนต้องการเปลี่ยนแปลงอยู่แล้ว แต่แนวคิดตามแบบฉบับของความเข้าใจในรายการเกี่ยวข้องกับการสร้างอาร์เรย์ / รายการที่ไม่มีมาก่อนโดยอ้างอิงการทำซ้ำบางส่วน แต่จริงๆแล้วคำจำกัดความที่เป็นทางการบางคำบอกว่าความเข้าใจในรายการไม่สามารถใช้แผนที่ได้เลยแม้แต่เวอร์ชันของฉันก็ไม่ใช่โคเชอร์ - แต่ใกล้เคียงที่สุดเท่าที่จะทำได้ใน Ruby
ทำเครื่องหมาย

5
ฉันไม่เข้าใจว่าตัวอย่าง Ruby ของคุณควรจะเป็นอะนาล็อกของตัวอย่าง Python ของคุณอย่างไร รหัส Ruby ควรอ่าน: squares = (0..9) .map {| x | x ** 2}
michau

4
ในขณะที่ @michau ถูกต้องจุดรวมของความเข้าใจในรายการ (ซึ่งมาร์คละเลย) คือความเข้าใจในรายการเองไม่ได้ใช้ไม่สร้างอาร์เรย์ - มันใช้เครื่องกำเนิดไฟฟ้าและกิจวัตรร่วมเพื่อทำการคำนวณทั้งหมดในลักษณะสตรีมโดยไม่ต้องจัดสรรพื้นที่เก็บข้อมูลเลย (ยกเว้น ตัวแปร temp) จนถึง (iff) ผลลัพธ์จะลงจอดในตัวแปรอาร์เรย์ - นี่คือจุดประสงค์ของวงเล็บเหลี่ยมในตัวอย่าง python เพื่อยุบความเข้าใจเป็นชุดของผลลัพธ์ Ruby ไม่มีสิ่งอำนวยความสะดวกที่คล้ายกับเครื่องกำเนิดไฟฟ้า
Guss

4
โอ้ใช่มันมี (ตั้งแต่ 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

ฉันได้พูดคุยในหัวข้อนี้กับ Rein Henrichs ซึ่งบอกฉันว่าวิธีแก้ปัญหาที่มีประสิทธิภาพดีที่สุดคือ

map { ... }.compact

สิ่งนี้สมเหตุสมผลเพราะหลีกเลี่ยงการสร้างอาร์เรย์กลางเช่นเดียวกับการใช้งานที่ไม่เปลี่ยนรูปEnumerable#injectและหลีกเลี่ยงการขยาย Array ซึ่งทำให้เกิดการจัดสรร โดยทั่วไปจะเหมือนกับของอื่น ๆ เว้นแต่คอลเลกชันของคุณจะมีองค์ประกอบศูนย์

ฉันไม่ได้เปรียบเทียบสิ่งนี้กับ

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

เป็นไปได้ว่าการใช้งาน C ของ Ruby Enumerable#selectก็ดีมากเช่นกัน


9

ทางเลือกอื่นที่จะทำงานในทุกการใช้งานและทำงานใน O (n) แทนเวลา O (2n) คือ:

some_array.inject([]){|res,x| x % 2 == 0 ? res << 3*x : res}

11
คุณหมายความว่ามันผ่านรายการเพียงครั้งเดียว ถ้าคุณใช้คำจำกัดความที่เป็นทางการ O (n) เท่ากับ O (2n) เพียงแค่ nitpicking :)
Daniel Hepper

1
@Daniel Harper :) ไม่เพียง แต่คุณจะถูกเท่านั้น แต่ยังรวมถึงกรณีทั่วไปด้วยการข้ามรายการหนึ่งครั้งเพื่อละทิ้งบางรายการและจากนั้นอีกครั้งเพื่อดำเนินการสามารถทำได้ดีกว่าในกรณีทั่วไป :)
Pedro Rolo

ในคำอื่น ๆ ที่คุณกำลังทำ2สิ่งที่nครั้งแทน1สิ่งที่nครั้งและหลังจากนั้นอีก1สิ่งที่nครั้ง :) ข้อดีอย่างหนึ่งที่สำคัญของการinject/ reduceคือว่ามันจะเก็บรักษาใด ๆnilค่าในลำดับการป้อนข้อมูลที่เป็นพฤติกรรมรายการ comprehensionly มากขึ้น
จอห์นลา Rooy


7

Enumerable มีgrepวิธีการที่อาร์กิวเมนต์แรกสามารถเป็นเพรดิเคต proc ได้และอาร์กิวเมนต์ที่สองที่เป็นทางเลือกคือฟังก์ชันการแม็ป ดังนั้นการทำงานต่อไปนี้:

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

สิ่งนี้ไม่สามารถอ่านได้เหมือนกับคำแนะนำอื่น ๆ (ฉันชอบselect.mapอัญมณีที่เข้าใจง่ายของ anoiaque หรือฮิสโตรแครต) แต่จุดแข็งของมันคือมันเป็นส่วนหนึ่งของไลบรารีมาตรฐานอยู่แล้วและเป็นแบบ single-pass และไม่เกี่ยวข้องกับการสร้างอาร์เรย์กลางชั่วคราว และไม่ต้องการค่านอกขอบเขตเช่นเดียวกับที่nilใช้ในcompactคำแนะนำการใช้งาน


4

มีความกระชับมากขึ้น:

[1,2,3,4,5,6].select(&:even?).map{|x| x*3}

2
หรือเพื่อความสุดยอดยิ่งขึ้นโดยไม่มีจุด[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]

ที่เหมาะกับฉัน นอกจากนี้ยังมีความสะอาด ใช่มันเหมือนกับmapแต่ฉันคิดว่าcollectทำให้เข้าใจรหัสได้มากขึ้น


select(&:even?).map()

ดูดีขึ้นจริง ๆ หลังจากดูด้านล่าง


2

เช่นเดียวกับที่ Pedro กล่าวไว้คุณสามารถหลอมรวมการโทรที่ถูกล่ามโซ่เข้าด้วยกันEnumerable#selectและEnumerable#mapหลีกเลี่ยงการข้ามผ่านองค์ประกอบที่เลือก นี่เป็นเรื่องจริงเนื่องจากEnumerable#selectเป็นความเชี่ยวชาญในการพับหรือinject. ฉันโพสต์แนะนำอย่างเร่งรีบสำหรับหัวข้อที่ Ruby subreddit

การแปลง Array ด้วยตนเองอาจเป็นเรื่องที่น่าเบื่อดังนั้นอาจมีใครบางคนสามารถเล่นกับcomprehendการนำไปใช้ของ Robert Gamble เพื่อทำให้รูปแบบนี้select/ mapรูปแบบสวยขึ้น


2

สิ่งนี้:

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

เรียกมันว่า:

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

ซึ่งผลตอบแทน:

=> [6, 12, 18]

เกิดอะไรขึ้นกับการกำหนดlazyArray แล้ว:(1..6).lazy{|x|x*3 if x.even?}
Guss

1

ทางออกอื่น แต่อาจไม่ใช่วิธีที่ดีที่สุด

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

หรือ

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

0

นี่เป็นวิธีหนึ่งในการเข้าถึงสิ่งนี้:

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 

ดังนั้นโดยพื้นฐานแล้วเรากำลังแปลงสตริงเป็นไวยากรณ์ทับทิมที่เหมาะสมสำหรับลูปจากนั้นเราสามารถใช้ไวยากรณ์ python ในสตริงเพื่อทำ:

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]

หรือถ้าคุณไม่ชอบลักษณะของสตริงหรือต้องใช้แลมบ์ดาเราสามารถละทิ้งความพยายามที่จะสะท้อนไวยากรณ์ของ python และทำสิ่งนี้:

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

0

Ruby 2.7 แนะนำfilter_mapซึ่งค่อนข้างบรรลุสิ่งที่คุณต้องการ (แผนที่ + ขนาดกะทัดรัด):

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

คุณสามารถอ่านเพิ่มเติมได้ที่นี่



-4

ฉันคิดว่ารายการที่เข้าใจได้มากที่สุดน่าจะเป็นดังต่อไปนี้:

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

เนื่องจาก Ruby อนุญาตให้เราวางเงื่อนไขหลังนิพจน์เราจึงได้ไวยากรณ์ที่คล้ายกับ Python เวอร์ชันของความเข้าใจรายการ นอกจากนี้เนื่องจากselectเมธอดไม่รวมสิ่งที่falseมีค่าเท่ากับค่าศูนย์ทั้งหมดจะถูกลบออกจากรายการผลลัพธ์และไม่จำเป็นต้องเรียกคอมแพ็คอย่างที่เป็นในกรณีที่เราเคยใช้mapหรือใช้collectแทน


7
ดูเหมือนจะไม่ได้ผล อย่างน้อยใน Ruby 1.8.6, [1,2,3,4,5,6] .select {| x | x * 3 ถ้า x% 2 == 0} ประเมินค่าเป็น [2, 4, 6] การนับจำนวน # select จะสนใจเฉพาะว่าบล็อกประเมินว่าเป็นจริงหรือเท็จไม่ใช่ค่าที่ส่งออก AFAIK
Greg Campbell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.