การสร้างผลิตภัณฑ์ย่อยใน Ruby
จากตัวอย่างตัวแปรฉันต้องการชุดค่าผสมทั้งหมดของตัวแปรทั้งหมด ในตัวอย่างฉันมี 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
แต่ละลูปที่ซ้อนกันของฉันเป็นที่ต้องการหรือมีการใช้งานที่ดีกว่านี้หรือไม่?
คำตอบ
ตัวอักษรสตริงค้าง
โครงสร้างข้อมูลที่ไม่เปลี่ยนรูปและรหัสที่ใช้งานได้ล้วนเป็นที่ต้องการเสมอเว้นแต่ต้องการความไม่แน่นอนและผลข้างเคียงเพื่อความชัดเจนหรือประสิทธิภาพ ใน 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
ซึ่งฉันคิดว่าเป็นโค้ดที่ดูดีอ่านง่ายเข้าใจง่าย