3 คะแนน โดย GN⁺ 2025-05-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • การย้าย คำสั่ง if ภายในฟังก์ชันขึ้นไปไว้ที่จุดเรียกใช้ ช่วยลด ความซับซ้อน ของโค้ดได้
  • การรวม การตรวจสอบเงื่อนไข และการจัดการทางแยกไว้ในที่เดียว ทำให้มองเห็นความซ้ำซ้อนและการตรวจสอบทางแยกที่ไม่จำเป็นได้ง่าย
  • สามารถใช้ การรีแฟกเตอร์แบบแยก enum (Dissolving enum Refactor) เพื่อป้องกันปัญหาที่เงื่อนไขเดียวกันกระจายอยู่ทั่วโค้ด
  • ลูป for ที่อิงกับ การประมวลผลแบบแบตช์ มีประสิทธิภาพในการเพิ่มสมรรถนะและปรับงานที่ทำซ้ำให้เหมาะสม
  • การผสานแพตเทิร์น if ขึ้นบน, for ลงล่าง ช่วยเพิ่มทั้งความอ่านง่ายและประสิทธิภาพของโค้ดไปพร้อมกัน

บันทึกสั้น ๆ เกี่ยวกับกฎที่เกี่ยวข้องกันสองข้อ

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

การรวมศูนย์ของ control flow และคำสั่งเงื่อนไข

  • control flow และ คำสั่ง if เป็นสาเหตุหลักของความซับซ้อนและบั๊กในโค้ด
  • การรวมคำสั่งเงื่อนไขขึ้นไปไว้ในระดับบน เช่น จุดเรียกใช้ แล้วให้มีเพียงฟังก์ชันเดียวที่รับผิดชอบการแตกแขนง ขณะที่งานจริงปล่อยให้เป็นซับรูทีนแบบเส้นตรง (straight-line) เป็นแพตเทิร์นที่มีประโยชน์
  • เมื่อการแตกแขนงและ control flow ถูกรวมไว้ในจุดเดียว จะมองเห็น ทางแยกที่ซ้ำซ้อน และ เงื่อนไขที่ไม่จำเป็น ได้ง่าย

ตัวอย่าง:

  • เมื่อมี if ซ้อนอยู่ในฟังก์ชัน f จะสังเกตเห็น dead code (Dead Branch) ได้ง่าย
  • แต่ถ้าการแตกแขนงกระจายไปตามหลายฟังก์ชัน (g, h) การมองเห็นสิ่งเหล่านี้จะยากขึ้น

การรีแฟกเตอร์แบบแยก enum (Dissolving enum Refactor)

  • หากโค้ดบรรจุการแตกแขนงจากคำสั่งเงื่อนไขเดียวกันไว้ผ่าน enum เป็นต้น การดึงเงื่อนไขขึ้นไประดับบนจะช่วยแยก “การแตกแขนง” ออกจาก “งานที่ทำ” ให้ชัดเจนขึ้น
  • เมื่อใช้แนวทางนี้ จะป้องกันปัญหาที่เงื่อนไขเดียวกันปรากฏ ซ้ำหลายครั้ง ในโค้ดได้

ตัวอย่าง:

  • สถานการณ์ที่เงื่อนไขการแตกแขนงเดียวกันถูกแสดงอยู่ทั้งในฟังก์ชัน f, g และใน enum E
  • สามารถทำให้โค้ดทั้งระบบเรียบง่ายขึ้นได้ด้วยการรวมให้เหลือการแตกแขนงระดับบนเพียงจุดเดียว

แนวคิดแบบ data-oriented (Data Oriented Thinking) และการประมวลผลแบบแบตช์

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

ตัวอย่าง:

  • ใช้ ฟังก์ชันประมวลผลแบบแบตช์ อย่าง frobnicate_batch(walruses) เป็นค่าเริ่มต้น

  • แล้วค่อยแปลงการทำงานกับออบเจ็กต์เดี่ยวให้เป็นกรณีพิเศษที่ประมวลผลผ่านลูป for ได้

  • แนวทางนี้มีบทบาทสำคัญในด้านการปรับประสิทธิภาพ โดยช่วยลดต้นทุนเริ่มต้นของงานขนาดใหญ่และเพิ่มความยืดหยุ่นด้านลำดับการประมวลผล

  • ยังสามารถใช้ SIMD (struct-of-array เป็นต้น) ได้ด้วย ทำให้ประมวลผลเฉพาะบางฟิลด์พร้อมกันก่อน แล้วจึงทำงานทั้งหมดต่อได้

กรณีใช้งานจริงและแพตเทิร์นที่แนะนำ

  • เช่นเดียวกับการคูณพหุนามด้วย FFT ที่เปิดให้คำนวณหลายจุดพร้อมกันได้ จึงสามารถดึงประสิทธิภาพออกมาได้สูงสุด
  • กฎการย้ายคำสั่งเงื่อนไขขึ้นบน และย้ายลูปลงล่าง สามารถนำมาใช้ร่วมกันได้

ตัวอย่าง:

  • แทนที่จะตรวจสอบเงื่อนไขเดียวกันซ้ำ ๆ ภายในลูป การดึงคำสั่งเงื่อนไขออกไปไว้นอกลูปจะช่วยลดการแตกแขนงภายในลูป และทำให้ การเพิ่มประสิทธิภาพและการเวกเตอร์ไรซ์ ทำได้ง่ายขึ้น
  • แนวทางนี้รับประกันประสิทธิภาพสูงได้แม้ใน data plane ของระบบขนาดใหญ่ เช่น การออกแบบของ TigerBeetle

บทสรุป

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

1 ความคิดเห็น

 
GN⁺ 2025-05-18
ความคิดเห็นบน Hacker News
  • แบบจำลองทางความคิดที่ค่อนข้างเฉพาะตัวของฉันคือ มองว่าสถานะต่าง ๆ หรือการไหลของโปรแกรมมีโครงสร้างเป็นต้นไม้ และเงื่อนไขทำหน้าที่ตัดกิ่งของต้นไม้นั้น ฉันอยากตัดแต่งกิ่งให้เร็วที่สุดเท่าที่จะทำได้ เพื่อลดจำนวนกิ่งที่ต้องประมวลผลต่อไป ไม่อยากตกอยู่ในสถานการณ์ที่ต้องไล่ประเมินและจัดการทุกกิ่งทีละอัน แล้วสุดท้ายค่อยมาตัดทิ้งทั้งก้อนทีเดียว ถ้ามองในอีกมุมหนึ่ง เงื่อนไขคือ "กระบวนการค้นหางานที่ไม่จำเป็น" ส่วนลูปคือ "งานจริง" ท้ายที่สุด ฟังก์ชันที่ฉันต้องการก็คือฟังก์ชันที่โฟกัสอย่างใดอย่างหนึ่งระหว่างการเดินสำรวจต้นไม้ของโปรแกรมกับการทำงานจริง
    • อยากเสนอแบบจำลองอีกแบบของฉันบ้าง ฉันมองว่า class คือคำนาม และ function คือคำกริยา
    • แบบจำลองทางความคิดของฉันจะปรับให้เข้ากับโลกที่โค้ดรูปธรรมที่ฉันเขียนอยู่มีตัวตนจริง มันเปลี่ยนไปตามลักษณะของโดเมน แพตเทิร์นโค้ดเดิม ขั้นของ data pipeline โปรไฟล์ด้านประสิทธิภาพ ฯลฯ ตอนแรกฉันก็พยายามสร้างกฎหรือ heuristic แบบนี้ แต่พอเขียนโค้ดไปเยอะก็พบว่ากฎเชิงนามธรรมแบบนี้แทบไม่มีความหมายในโลกจริง หลายครั้งมันกลายเป็นแค่ว่าตั้งชื่อฟังก์ชันหรือชื่อตัวแปรตัวเดียวไว้ แล้วกฎนั้นใช้ได้แค่ใน “เกาะโค้ด” เล็ก ๆ นั้น แต่ใน codebase จริง มักมีเหตุผลอยู่แล้วที่ฟังก์ชันเหล่านั้นไม่ได้ถูกรวมเข้าด้วยกัน ตัวอย่างเช่นประเด็นเรื่อง “ความซ้ำซ้อนและเงื่อนไขที่ตายแล้ว” เป็นกฎที่ใช้ได้เพราะตั้งสมมติฐานสบาย ๆ ว่าฟังก์ชันนั้นถูกเรียกจากที่เดียวเท่านั้น แต่ในความจริง หลายอย่างถูกแยกไว้ด้วยเหตุผลอื่น
    • ฉันว่าเป็นแบบจำลองที่ดีมาก
  • กฎที่ทั่วไปกว่านั้นคือ ให้วางเงื่อนไขไว้ใกล้กับแหล่งที่มาของอินพุตมากที่สุด ประเด็นสำคัญคือระบุจุดที่ข้อมูลจากภายนอกเข้ามาในโปรแกรมให้เร็วที่สุดเท่าที่ทำได้ (รวมถึงข้อมูลที่ดึงมาจากบริการอื่น) แล้วสร้างการรับประกันให้ได้มากที่สุดก่อนที่ข้อมูลนั้นจะไปถึง core logic (โดยเฉพาะก่อนถึงส่วนที่ใช้ทรัพยากรมาก) การแสดงสิ่งนี้อย่างชัดเจนใน type ก็เป็นเรื่องที่ดีมากเช่นกัน
    • แบบนี้จะไม่ทำให้เวลาทำความเข้าใจ core logic ยากขึ้นหรือ ว่ามันตั้งอยู่บนสมมติฐานอะไรบ้าง? แบบนี้ไม่ต้องไล่ดู call chain ทั้งระบบเลยหรือ?
  • คำแนะนำว่า “ถ้ามีเงื่อนไข if อยู่ในฟังก์ชัน ให้พิจารณาว่าย้ายมันไปไว้ฝั่ง caller ได้หรือไม่” มีตัวอย่างโต้แย้งเยอะเกินไป ถ้าฟังก์ชันนั้นถูกเรียกจาก 37 ที่ จะให้ทำซ้ำ if เดิมในทุกจุดเรียกอย่างนั้นหรือ? เช่นจะให้ย้าย if แบบนี้ออกจากฟังก์ชันอย่าง getaddrinfo หรือ EnterCriticalSection ได้จริงหรือ? ฉันคิดว่าการแปลงแบบนี้ควรพิจารณาเฉพาะกรณีที่มีการเรียกแค่ราว ๆ สองที่ และการตัดสินใจนั้นอยู่นอกขอบเขตความรับผิดชอบของฟังก์ชัน วิธีหนึ่งคือเขียนฟังก์ชันที่ทำแต่เงื่อนไข แล้วมอบหมายให้ helper function ไปทำงานจริง และถ้าจำเป็นต้องย้ายเงื่อนไขออกไปนอกลูป ก็อาจเปิดให้ caller ใช้ helper เงื่อนไขระดับล่างได้โดยตรง แต่หัวใจของการถกเถียงนี้คือเรื่อง “optimization” และ optimization ก็มักขัดกับการออกแบบโปรแกรมที่ดีกว่าอยู่บ่อย ๆ การที่ caller ไม่จำเป็นต้องรู้เรื่องเงื่อนไขอาจเป็นการออกแบบที่ดีกว่าก็ได้ ภาวะแบบนี้เจอบ่อยใน OOP เช่นกัน โดย “if” ที่ว่านี้ในความจริงอาจถูกแทนด้วย method dispatch การดึง dispatch แบบนี้ออกไปนอกลูปก็อาจไปเสียดสีกับหลักการออกแบบได้ เช่นเวลาวาดภาพลงบน canvas การใช้เมธอดอย่าง blit จะเป็นแนวทางที่เหมาะกว่า เรียก putpixel ซ้ำ ๆ ทุกครั้ง
    • ถ้าฟังก์ชันถูกเรียกจาก 37 ที่ ก็น่าจะต้องมีการรีแฟกเตอร์โค้ดอยู่เหมือนกัน ถ้าจะตอบคำถามนั้นก็ต้องบอกว่า ขึ้นอยู่กับบริบท แม้ DRY จะฟังดูเหมือนคำตอบที่ถูก แต่ต้องดูตัวอย่างโค้ดจริงก่อนตัดสินใจ ถ้าเป็น library มันอยู่ตรงขอบเขตความเป็นเจ้าของ ดังนั้นแต่ละฝั่งต้องจัดการข้อมูลและความรับผิดชอบของตัวเอง ฟังก์ชันอย่าง EnterCriticalSection ควรมีการตรวจสอบที่เข้มงวดตรงจุดเข้าใช้งาน (รวมถึงเงื่อนไขด้วย) แต่ใน application code การย้าย if ไปฝั่ง caller ก็อาจไม่เป็นปัญหา สำหรับ library หรือโค้ดแกนกลาง การผลัก control flow ไปไว้ที่ขอบเป็นเรื่องเหมาะสม ภายในโดเมนที่เราดูแลเอง ก็ควรวาง control flow ไว้ที่ขอบเช่นกัน แต่แน่นอนว่ากฎพวกนี้เป็นเพียงแนวทางปฏิบัติเท่านั้น คนที่ตัดสินได้อย่างมีเหตุผลตามสถานการณ์ต้องเป็นคนที่เข้าใจบริบท
  • ตัวอย่างการรีแฟกเตอร์แบบ “dissolving enum” นั้นจริง ๆ แล้วคือแพตเทิร์น polymorphism เราสามารถแทน match ด้วยการเรียกเมธอดแบบ polymorphic ได้ จุดประสงค์ของแนวทางนี้คือแยกช่วงเวลาที่ตัดสินใจเลือกการแตกแขนงตั้งต้น ออกจากช่วงเวลาที่พฤติกรรมจริงถูกเรียกใช้ การจำแนกเคสถูกเก็บไว้ใน object (ในที่นี้คือค่า enum) หรือ closure ดังนั้นจึงไม่ต้องทำซ้ำทุกครั้งตอนเรียกใช้ ถ้าการจำแนกเคสเปลี่ยน ก็แก้แค่จุดแตกแขนง โดยไม่ต้องไปแก้ที่ตำแหน่งที่พฤติกรรมจริงเกิดขึ้น ข้อเสียคือมี trade-off ระหว่างความสะดวกในการมองเห็นการแตกแขนงของแต่ละเคสโดยตรง กับการที่โค้ดเกิดการพึ่งพารายชื่อเคสในระดับโค้ด
  • บางครั้งฉันชอบเก็บเงื่อนไขไว้ภายในฟังก์ชัน เพราะมันช่วยป้องกันไม่ให้ caller เรียกลำดับฟังก์ชันผิดพลาดได้โดยตั้งใจ เช่นกรณีที่ต้องการการรับประกันเรื่อง idempotency ก็อาจเช็กก่อนว่าสถานะนี้ถูกประมวลผลไปแล้วหรือยัง ถ้ายังก็ค่อยทำงาน ถ้าย้ายเงื่อนไขนี้ออกไปที่จุดเรียก ผู้เรียกทุกคนจะต้องทำตามขั้นตอนนั้นให้ถูกต้องเองจึงจะยังคงรับประกัน idempotency ได้ เท่ากับ abstraction นี้ไม่สามารถให้การรับประกันดังกล่าวได้ ฉันเลยสงสัยว่าควรนำแนวคิดนี้ไปใช้อย่างไรในสถานการณ์แบบนี้ อีกตัวอย่างคือเวลาต้องทำชุดของการตรวจสอบทั้งหมดภายใน database transaction แล้วค่อยทำงานจริง ก็จะลังเลว่าควรวางเช็กเหล่านั้นไว้ตรงไหน
    • ดูเหมือนคุณตอบคำถามตัวเองไปแล้ว ถ้าย้ายเงื่อนไขไปที่จุดเรียก ฟังก์ชันนั้นก็จะไม่เป็น idempotent อีกต่อไป และแน่นอนว่าจะรับประกันไม่ได้ ถ้าคุณต้องใส่ลอจิกจัดการสถานะในทุกฟังก์ชันเพื่อรับประกัน idempotency ก็อาจแปลว่าคุณกำลังเขียนโค้ดที่แปลกพอสมควร และกำลังยัด business logic มากเกินไปไว้ในฟังก์ชันเดียว โค้ดแบบ idempotent แบ่งกว้าง ๆ ได้สองแบบ แบบแรกคือ data model หรือการดำเนินการนั้น idempotent อยู่แล้ว ซึ่งกรณีนี้ไม่จำเป็นต้องใส่ใจลำดับการประมวลผลมากนัก แบบที่สองคือการสร้าง abstraction ที่ให้ idempotency สำหรับ business operation ที่ซับซ้อนกว่า ซึ่งต้องมีลอจิกอย่าง rollback หรือ abstraction บนการ apply แบบ atomic และกรณีนี้ไม่ใช่เรื่องที่จะยัดใส่ฟังก์ชันเดี่ยวแบบง่าย ๆ ได้
    • อีกวิธีคือทำฟังก์ชันภายในที่ไม่มีการเช็ก แล้วมีฟังก์ชัน wrapper ภายนอกคอยเช็กก่อนเรียกฟังก์ชันภายใน
  • เครื่องมือสแกนความซับซ้อนของโค้ดมักมีแนวโน้มผลัก if ลงล่างไปเรื่อย ๆ แต่บทความนี้กลับแนะนำในทางตรงกันข้าม คือยก if ขึ้นบนไปยังฟังก์ชันระดับสูงกว่า วิธีนั้นทำให้รวมศูนย์ลอจิกการแตกแขนงที่ซับซ้อนไว้ในฟังก์ชันเดียว และมอบหมายงานเชิงรูปธรรมจริง ๆ ให้ subroutine จัดการ
    • วิธีแก้คือแยก “การตัดสินใจ” ออกจาก “การลงมือทำ” เป็นไอเดียที่ฉันเรียนมาจาก Bertrand Meyer เช่น if (weShouldDoThis()) { doThis(); } ประมาณนี้ และถ้าแยกการตรวจสอบแต่ละอย่างเป็นฟังก์ชันต่างหาก ก็จะช่วยให้ทดสอบและจัดการความซับซ้อนได้ง่ายขึ้น
    • รายงานจากเครื่องมือสแกนโค้ดควรถูกตั้งข้อสงสัยอย่างจริงจัง sonarqube และเครื่องมือแนวนี้ชอบรายงานแม้แต่ “code smell” ที่ไม่ใช่บั๊กจริง การพยายามแก้ “โค้ดที่ไม่ใช่ปัญหา” แบบนี้เสี่ยงจะสร้างบั๊กใหม่ และทำให้เสียเวลาที่ควรเอาไปจัดการปัญหาสำคัญจริง ๆ
    • optimization แบบนี้ส่วนใหญ่มักเป็นแค่ “local optimum” กล่าวคือพอมี requirement ใหม่หรือกรณียกเว้นเพิ่มขึ้น ลอจิกการแตกแขนงก็ต้องไปอยู่นอกลูปด้วย พอถึงตอนนั้น ถ้ามีเงื่อนไขปนกันทั้งในและนอกลูปก็จะเข้าใจยากมาก ถ้ามั่นใจว่าเงื่อนไขจำเป็นต้องอยู่แค่ในลูปก็เก็บไว้แบบนั้นได้ แต่ถ้าไม่แน่ใจ ฉันกลับคิดว่าควรยอมออกแบบเผื่อไว้มากขึ้นสักหน่อย และยอมให้โค้ดยาวขึ้นแต่เข้าใจง่ายกว่า ฉันเคยเจอแบบนี้ตอนใช้ Haskell ถ้าไล่หาความกระชับและการ optimize แบบ local optimum มากเกินไป พอ requirement เปลี่ยนเพียงนิดเดียว โค้ดจะหยุดแสดงเจตนาของการออกแบบแล้วเหลือแค่ตรรกะล้วน ๆ และการเปลี่ยนแปลงเล็กน้อยก็ทำให้ต้อง unroll โค้ดอย่างหนัก
    • เครื่องมือสแกนความซับซ้อนของโค้ดเป็นสิ่งที่ฉันไม่ค่อยพอใจมาตลอด แม้แต่ฟังก์ชันใหญ่ที่อ่านง่ายมันก็ยังบ่น การวางลอจิกไว้ในที่เดียวช่วยให้เข้าใจบริบทโดยรวมได้ง่าย แต่เวลาจะแยกฟังก์ชันก็ต้องระวังไม่ให้บริบทที่แท้จริงหายไป
    • เมื่อวานเพิ่งมีการคุยกันในเธรดเรื่อง LLM เกี่ยวกับ “เครื่องมือที่นักพัฒนาทุกคนยอมรับใช้ทั้งที่ไม่น่าเชื่อถือ” ตอนนี้ฉันได้คำตอบแล้ว…
  • ในบางกรณี ควรคิดกลับด้านแล้วใช้ SIMD แทน เช่นบน AVX-512 เป็นต้น โค้ดที่มีการแตกแขนงสามารถทำให้เป็นโค้ดแบบไม่มี branch ได้ด้วย vector mask register ตัวอย่างเช่น if ภายใน for อาจจัดการง่ายกว่า if นอก for และยังให้ประสิทธิภาพการเข้าถึงหน่วยความจำที่ดีกว่า ยกตัวอย่างแบบเป็นรูปธรรม ถ้ามีการคำนวณว่า ถ้าเป็นเลขคี่ให้ +1 ถ้าเป็นเลขคู่ให้ -2 ปกติแล้วแต่ละรอบลูปต้องวิ่งผ่าน branch แต่ถ้าทำแบบ SIMD จะประมวลผล int ได้พร้อมกันทีละ 16 ค่าและไม่มี branch ด้วย ถ้า compiler ทำ vectorization ได้ดี ก็จะเปลี่ยนโค้ดเดิมให้เป็นเวอร์ชัน optimize แบบไม่มี branch ได้
    • ฉันว่าตัวอย่าง before ที่ยกมาดูไม่ค่อยตรงกับประเด็นของบทความเท่าไร และจริง ๆ แล้วเวอร์ชัน SIMD ที่ optimize แล้วต่างหากที่เข้ากับสาระของบทความ ในตัวอย่างนั้น if ภายใน for เป็นการแตกแขนงที่ขึ้นกับข้อมูล จึงดึงขึ้นไปข้างบนได้ยาก ถ้าอัลกอริทึมมีโครงสร้างเป็น if (length % 2 == 1) { ... } else { ... } แบบนี้คือใช้เงื่อนไขนอกลูปอยู่แล้ว แน่นอนว่าควรย้ายเงื่อนไขประเภทนี้ขึ้นไปไว้เหนือ for ในเวอร์ชัน SIMD นั้น if หายไปเลย ซึ่งเป็นแพตเทิร์นโค้ดในอุดมคติ และน่าจะเป็นแนวที่ผู้เขียนบทความชอบด้วย
    • ฉันเองก็นึกถึงโค้ดที่แตกแขนงตามค่าของแต่ละองค์ประกอบในลูป for ทันที มีใครพอรู้ไหมว่าการที่ compiler จะทำ auto-vectorization ให้โค้ดแบบนี้มันยากแค่ไหน? ฉันอยากรู้ว่าขอบเขตมันอยู่ตรงไหน
  • โดยส่วนตัวฉันไม่คิดว่านี่เป็นกฎที่ “ดี” นัก แม้จะมีกรณีที่ใช้ได้ แต่มันขึ้นกับบริบทมากเกินไปจนยากจะสรุปแบบฟันธง ให้ความรู้สึกเหมือนกฎการสะกดภาษาอังกฤษที่มีข้อยกเว้นเยอะเกินไป จนแทบเรียกว่าเป็นกฎไม่ได้
  • ลิงก์การสนทนาในตอนนั้น (2023) (662 คะแนน, 295 ความคิดเห็น) https://news.ycombinator.com/item?id=38282950
  • ฉันเคยเจอแนวคิดคล้ายกันนี้ใน 99 Bottles of OOP ของ Sandi Metz มันไม่ใช่สไตล์ของฉัน แต่ก็เห็นด้วยว่าการยกตรรกะการแตกแขนงขึ้นไปไว้ด้านบนสุดของ call stack นั้นมีประโยชน์ โดยเฉพาะใน codebase ที่เคยส่ง flag ผ่านหลายชั้นของระบบมาก่อน มันยิ่งเห็นชัด https://sandimetz.com/99bottles
    • ฉันนึกถึงบทความ “The Wrong Abstraction” ของผู้เขียนคนเดียวกันขึ้นมาทันที การแตกแขนงภายใน for คือการสร้าง abstraction แบบ “for คือกฎ ส่วนเงื่อนไขคือพฤติกรรม” แต่พอมี requirement ใหม่ abstraction แบบนี้ก็พัง แล้วเราจะเริ่มยัดพารามิเตอร์เพิ่มหรือใส่กรณียกเว้นมากขึ้นจนโค้ดเข้าใจยาก ถ้าเริ่มต้นโดยไม่สร้าง abstraction ตั้งแต่แรก ผลลัพธ์อาจชัดเจนกว่าและดูแลรักษาง่ายกว่า https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction