การสร้างผลิตภัณฑ์ย่อยใน Ruby

Oct 31 2020

จากตัวอย่างตัวแปรฉันต้องการชุดค่าผสมทั้งหมดของตัวแปรทั้งหมด ในตัวอย่างฉันมี 3x3x2 = 18 ตัวแปร

## SAMPLE VARIANTS
sizes = ['small', 'medium', 'large']
colors = ['red', 'green', 'blue']
materials = ['cotton', 'linen']

## ITERATE TO ALL VARIANTS
titles = []
sizes.each do |size|
  colors.each do |color|
    materials.each do |material|
      ## PUT THE VARIANT IN THE NEW ARRAY
      titles.push("#{size} - #{color} - #{material}")
    end
  end
end

puts titles.inspect

แต่ละลูปที่ซ้อนกันของฉันเป็นที่ต้องการหรือมีการใช้งานที่ดีกว่านี้หรือไม่?

คำตอบ

4 JörgWMittag Oct 31 2020 at 17:22

ตัวอักษรสตริงค้าง

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

# frozen_string_literal: true

โดยทั่วไปแล้วจะแนะนำให้เพิ่มความคิดเห็นนี้ในไฟล์ทั้งหมดของคุณ

ตัวอักษร "เปอร์เซ็นต์" ของอาร์เรย์คำ

Ruby มีตัวอักษรอาร์เรย์พิเศษสำหรับอาร์เรย์ของสตริงคำเดียวที่สามารถทำให้โค้ดของคุณอ่านง่ายขึ้นโดยการลดจำนวน "syntax fluff" รอบ ๆ เนื้อหาจริง

ตัวอักษรเริ่มต้นด้วยเครื่องหมาย%wหรือ%W(คิดว่า "word" หรือ "witespace-Separated") %wพฤติกรรมเช่นสายเดียวที่ยกมาคือไม่ได้ดำเนินการแก้ไขและไม่สนับสนุนหนีตัวละครอื่น ๆ กว่าและ\' ทำงานเหมือนสตริงที่ยกมาสองครั้ง\\%W

ดังนั้นจุดเริ่มต้นของสคริปต์ของคุณอาจมีลักษณะดังนี้:

# frozen_string_literal: true

## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]

เช่นเดียวกับตัวอักษรเปอร์เซ็นต์ทั้งหมดคุณสามารถเลือกตัวคั่นที่คุณต้องการใช้ได้อย่างอิสระเพื่อให้ตัวคั่นไม่เกิดขึ้นภายในลิเทอรัล ตัวอย่างเช่นคุณสามารถใช้|เป็นตัวคั่นที่,, @สิ่งที่คุณต้องการ:

sizes = %w@small medium large@
colors = %w@red green blue@
materials = %w@cotton linen@

อักขระตัวแรกหลังwกำหนดตัวคั่น ตัวคั่นมีสองรูปแบบ: จับคู่และไม่จับคู่ ด้วยตัวคั่นที่ไม่มีการจับคู่อักขระเดียวกันจะลงท้ายตัวอักษรตามตัวอย่างที่สอง ด้วยตัวคั่นที่จับคู่ตัวคั่นปิดที่ตรงกันจะสิ้นสุดตัวอักษรเช่นเมื่อคุณเริ่มต้นด้วย<คุณปิดด้วย>ฯลฯ ดูตัวอย่างแรก

ผ้าสำลี

คุณควรเรียกใช้ตัววิเคราะห์แบบลินเตอร์หรือแบบคงที่ในโค้ดของคุณ Rubocopเป็นที่นิยม แต่ก็มีคนอื่น ๆ

Rubocop สามารถตรวจจับการปรับปรุงสไตล์ทั้งหมดที่ฉันได้กล่าวไว้ข้างต้นและยังสามารถแก้ไขรายการทั้งหมดที่ฉันระบุไว้โดยอัตโนมัติ

ฉันได้ตั้งค่าตัวแก้ไขของฉันเพื่อให้มันเรียกใช้ Rubocop โดยอัตโนมัติด้วยการแก้ไขอัตโนมัติทันทีที่ฉันกด "บันทึก"

ผลลัพธ์ของการแก้ไขอัตโนมัติมีลักษณะดังนี้:

# frozen_string_literal: true

## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]

## ITERATE TO ALL VARIANTS
titles = []
sizes.each do |size|
  colors.each do |color|
    materials.each do |material|
      ## PUT THE VARIANT IN THE NEW ARRAY
      titles.push("#{size} - #{color} - #{material}")
    end
  end
end

puts titles.inspect

puts foo.inspect

Kernel#pเป็นวิธีการดีบักที่ต้องการ มันทำสิ่งเดียวกัน แต่เป็นสำนวนมากกว่าและได้รับการออกแบบมาโดยเฉพาะสำหรับการแก้ไขจุดบกพร่องอย่างรวดเร็ว (ดังนั้นชื่ออักขระเดียว)

บรรทัดสุดท้ายก็เป็นได้

p titles

นอกจากนี้ยังKernel#putsส่งคืนnilแต่Kernel#pส่งคืนอาร์กิวเมนต์ของมันดังนั้นคุณสามารถโยนมันเป็นชุดนิพจน์ยาว ๆ ได้อย่างรวดเร็วโดยไม่ต้องเปลี่ยนผลลัพธ์

ช่องว่างแนวตั้ง

รหัสของคุณอาจใช้ช่องว่างแนวตั้งเพื่อให้โค้ดมีพื้นที่หายใจมากขึ้น ฉันขอแนะนำอย่างน้อยก็แยกการเริ่มต้นที่จุดเริ่มต้นของลูป:

titles = []

sizes.each do |size|
  colors.each do |color|
    materials.each do |material|
      ## PUT THE VARIANT IN THE NEW ARRAY
      titles.push("#{size} - #{color} - #{material}")
    end
  end
end

ตัวดำเนินการ "พลั่ว" <<

Array#pushไม่ใช่สำนวน อย่างแม่นยำยิ่งขึ้นมันเป็นเพียงสำนวนถ้าคุณใช้อาร์เรย์เป็นสแต็กคุณจะใช้Array#pushและArray#popเนื่องจากเป็นชื่อมาตรฐานสำหรับการดำเนินการสแต็ก

วิธีที่เป็นสำนวนในการต่อท้ายสิ่งอื่นคือตัวดำเนินการพลั่วในกรณีนี้Array#<<ก็ควรจะเป็นเช่นนั้น

titles << "#{size} - #{color} - #{material}"

ตัวทำซ้ำ

ใน Ruby มันเป็นสำนวนที่จะใช้ตัววนซ้ำระดับสูง ในโค้ดของคุณคุณใช้ตัวทำซ้ำแทนการวนซ้ำอยู่แล้วซึ่งเป็นสิ่งที่ดี อย่างไรก็ตามeachเป็นระดับต่ำสุดของตัวทำซ้ำทั้งหมด มันเทียบเท่ากับFOREACH-OFลูปเป็นหลัก ไม่มีความหมายระดับสูงกว่าและขึ้นอยู่กับการกลายพันธุ์และผลข้างเคียง

เมื่อใดก็ตามที่คุณมีรูปแบบของ "การเตรียมผลให้ห่วงคอลเลกชันผนวกกับผลที่ได้ผลกลับ" ที่เป็นพับ มีการใช้งานการพับสองแบบในไลบรารีหลักของ Ruby injectและeach_with_object. injectเป็นสิ่งที่ใช้งานได้มากกว่าeach_with_objectเป็นสิ่งที่จำเป็นมากขึ้น ดังนั้นในตอนนี้เราจะใช้each_with_objectที่นี่เนื่องจากโค้ดยังค่อนข้างจำเป็นและนั่นทำให้ความสัมพันธ์ระหว่างโค้ดเก่าและโค้ดใหม่ชัดเจนยิ่งขึ้น

เป็นการเปลี่ยนแปลงทั่วไป

accumulator = some_initial_value

collection.each do |element|
  accumulator = do_something_with(accumulator, element)
end

กลายเป็น

accumulator = collection.inject(some_initial_value) do |accumulator, element|
  do_something_with(accumulator, element)
end

หรือ

collection.each_with_object(some_initial_value) do |element, accumulator|
  do_something_with(accumulator, element)
end

ในกรณีของคุณจะมีลักษณะดังนี้:

titles = []

sizes.each do |size|
  colors.each do |color|
    materials.each do |material|
      ## PUT THE VARIANT IN THE NEW ARRAY
      titles << "#{size} - #{color} - #{material}"
    end
  end
end

กลายเป็น

titles = []

sizes.each_with_object(titles) do |size, titles|
  colors.each_with_object(titles) do |color, titles|
    materials.each_with_object(titles) do |material, titles|
      ## PUT THE VARIANT IN THE NEW ARRAY
      titles << "#{size} - #{color} - #{material}"
    end
  end
end

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

titles = sizes.inject([]) do |acc, size|
  colors.inject(acc) do |acc, color|
    materials.inject(acc) do |acc, material|
      ## PUT THE VARIANT IN THE NEW ARRAY
      acc + ["#{size} - #{color} - #{material}"]
    end
  end
end

Linter มาเยือนแล้ว

Rubocop จริงบ่นเกี่ยวกับการใช้ของฉันเงาด้านนอกกับด้านในaccacc

ฉันไม่เห็นด้วย. คุณไม่ควรกลัวที่จะปิดใช้งานหรือกำหนดค่ากฎใหม่ใน Linter เพื่อให้เข้ากับสไตล์ของคุณ

อย่างไรก็ตามโปรดทราบว่าการเขียนโปรแกรมเป็นกีฬาประเภททีม หากคุณกำลังแก้ไขโค้ดให้ใช้สไตล์ที่มีอยู่ หากคุณเป็นส่วนหนึ่งของทีมให้ใช้รูปแบบของทีม หากคุณเขียนโอเพนซอร์สโค้ดให้ใช้รูปแบบของโครงการ หากคุณเริ่มต้นโครงการของคุณเองนำมาใช้รูปแบบของชุมชน (ไม่ได้สร้างสไตล์ของคุณเองสำหรับโครงการของคุณจนกว่าโครงการของคุณมีขนาดใหญ่และประสบความสำเร็จพอที่จะมีชุมชนที่เป็นอิสระของตัวเอง)

ทัศนศึกษา: ลักษณะทั่วไปของการพับ ( inject/ each_with_object)

เมื่อฉันเขียนไว้ข้างต้นว่าคุณสามารถเขียนการวนซ้ำนี้ใหม่โดยใช้injectหรือeach_with_objectนั่นเป็นคำสั่งที่ทำให้รู้สึกตึง ฉันไม่จำเป็นต้องอ่านรหัสเพื่อสร้างคำสั่งนี้

ปรากฎว่าการพับเป็นแบบ "ทั่วไป" ทุกย้ำกว่าคอลเลกชันสามารถใช้แสดงพับ ซึ่งหมายความว่าถ้าเราจะลบทุกวิธีออกจากEnumerableยกเว้นinjectเราสามารถนำEnumerableโมดูลทั้งหมดมาใช้ใหม่ได้อีกครั้งโดยไม่ต้องใช้อะไรเลยinjectนอกจาก ตราบเท่าที่เรามีinjectเราสามารถทำอะไรก็ได้

ตัวทำซ้ำใช้เวลา 2

ดังนั้นสิ่งที่เราทำจนถึงตอนนี้คือแทนที่ตัววนซ้ำระดับต่ำด้วยตัววนซ้ำระดับสูงกว่า

อย่างไรก็ตามเรายังไม่ได้ทำ สิ่งที่เรากำลังทำคือเรานำองค์ประกอบสามอย่างจากคอลเลกชั่นทั้งสามของเรามาต่อกันและใส่ลงในคอลเลกชั่นใหม่ ดังนั้นสิ่งที่เรากำลังทำก็คือการเปลี่ยนองค์ประกอบ (หรือสามองค์ประกอบ) หรือ "แมป" แต่ละองค์ประกอบให้เป็นองค์ประกอบใหม่

สิ่งนี้เรียกว่าแผนที่และยังมีอยู่ใน Ruby Enumerable#mapด้วย

ในที่สุดรหัสของเราจะมีลักษณะดังนี้:

titles = sizes.map do |size|
  colors.map do |color|
    materials.map do | material|
      "#{size} - #{color} - #{material}"
    end
  end
end

ผลที่ได้นี้เป็นจริงไม่ถูกต้อง: เราได้รับอาร์เรย์ triply-ซ้อนกันเพราะเรามี Enumerable#maptriply-ซ้อนกัน

เราได้Array#flattenผลลัพธ์ แต่มีวิธีที่ดีกว่าEnumerable#flat_map:

titles = sizes.flat_map do |size|
  colors.flat_map do |color|
    materials.map do | material|
      "#{size} - #{color} - #{material}"
    end
  end
end

สิ่งที่เราทำที่นี่ก็จะเข้ามาแทนที่โดยทั่วไปสูงระดับ iterator พับ (ซึ่งสามารถทำอะไร ) มี จำกัด มากขึ้น iterator ความเชี่ยวชาญมากขึ้นระดับสูงแผนที่ เราสามารถถ่ายทอดความหมายของเราให้กับผู้อ่านได้ดีขึ้น แทนที่จะคิดว่า "เอาล่ะที่นี่เรามีตัวสะสมและองค์ประกอบและเราทำบางอย่างกับองค์ประกอบนั้นแล้วต่อท้ายเข้ากับตัวสะสม ... อาฉันเห็นว่าเรากำลังเปลี่ยนองค์ประกอบแต่ละส่วน" ผู้อ่านเพียงแค่เห็นmapและรู้ทันทีว่าmapเปลี่ยนองค์ประกอบ

วิธีการอาร์เรย์

มีไม่มากที่เราสามารถปรับปรุงโค้ดได้โดยใช้ตัววนซ้ำ แต่มีวิธีการอื่น ๆ อีกมากมายทั้งในEnumerablemixinและระดับArray

ดังนั้นลองมากลับขั้นตอนและคิดเกี่ยวกับสิ่งที่เรากำลังจริงทำอะไรที่นี่: เราจะสร้างคาร์ทีเซียนสินค้าในสามของอาร์เรย์ และอาจไม่น่าแปลกใจที่มีวิธีการคำนวณผลคูณของอาร์เรย์ที่ตั้งชื่ออย่างสร้างสรรค์Array#product:

titles = sizes.product(colors, materials).map do |size, color, material|
  "#{size} - #{color} - #{material}"
end

Array#join

ในการปรับปรุงครั้งสุดท้ายเรามาดูกันว่าบล็อกกำลังทำอะไรอยู่คือ "การรวม" ตัวแปรทั้งสามเข้าด้วยกัน และอีกครั้งมีวิธีการอยู่แล้วArray#join:

titles = sizes.product(colors, materials).map do |variant|
  variant.join(' - ')
end

ผลลัพธ์สุดท้าย

ดังนั้นในท้ายที่สุดสิ่งทั้งหมดมีลักษณะดังนี้:

# frozen_string_literal: true

## SAMPLE VARIANTS
sizes = %w[small medium large]
colors = %w[red green blue]
materials = %w[cotton linen]

titles = sizes.product(colors, materials).map do |variant|
  variant.join(' - ')
end

p titles

ซึ่งฉันคิดว่าเป็นโค้ดที่ดูดีอ่านง่ายเข้าใจง่าย