3 คะแนน โดย GN⁺ 2025-05-18 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หากย้ายการแตกแขนงเงื่อนไขภายในฟังก์ชันขึ้นไปยังฝั่งผู้เรียก และย้ายงานที่ทำซ้ำกับชุดข้อมูลลงมาเป็น การดำเนินการแบบแบตช์ แทนการเรียกทีละรายการ จะช่วยให้ทั้งโฟลว์ของโค้ดและประสิทธิภาพเรียบง่ายขึ้นได้
  • เมื่อย้าย if ขึ้นไป จะรับประกันเงื่อนไขตั้งต้นด้วย type หรือ assert ได้ง่ายขึ้น และลด การตรวจสอบซ้ำ กับการแตกแขนงที่ไม่จำเป็นภายในฟังก์ชัน
  • หากตรรกะการแตกแขนงกระจายอยู่ในหลายฟังก์ชัน จะหาเงื่อนไขที่ตายแล้วหรือการตัดสินใจซ้ำได้ยาก และโครงสร้างที่สร้าง enum แล้ว match อีกครั้งก็อาจกลายเป็นรูปแบบที่ ทำซ้ำการแตกแขนงในรูปของโครงสร้างข้อมูล เช่นกัน
  • เมื่อย้าย for ลงมา จะประมวลผลหลายอ็อบเจกต์พร้อมกันเพื่อกระจายต้นทุนเริ่มต้น และเปิดช่องให้ปรับแต่งได้ เช่น การจัดลำดับการประมวลผลใหม่หรือ vectorization
  • เมื่อใช้กฎทั้งสองร่วมกัน จะลดการประเมินเงื่อนไขซ้ำภายในลูปและการแตกแขนงใน hot loop ทำให้โครงสร้างใกล้เคียงกับการย้ายต้นทุนการตัดสินใจของ control plane ไปเป็นการประมวลผลแบบแบตช์ใน data plane

ย้าย if ขึ้นไปยังฝั่งผู้เรียก

  • หากมีเงื่อนไข if ภายในฟังก์ชัน สิ่งแรกที่พิจารณาได้คือย้ายเงื่อนไขนั้นไปยัง ผู้เรียก ได้หรือไม่
    • ฟังก์ชันที่รับ Walrus โดยตรงเป็นรูปแบบที่เรียบง่ายกว่าฟังก์ชันที่รับ Option<Walrus> แล้วคืนค่าหากเป็น None
    • แทนที่ฟังก์ชันจะตรวจสอบเงื่อนไขตั้งต้นแล้ว “ไม่ทำอะไร” ผู้เรียกสามารถตรวจสอบก่อน แล้วรับประกันด้วย type หรือ assert ได้
  • วิธีการย้ายการตรวจสอบเงื่อนไขตั้งต้นขึ้นไปอาจแพร่ไปทั่วเส้นทางการเรียก และท้ายที่สุดจะเป็นแรงจูงใจให้ลด จำนวนครั้งของการตรวจสอบ
  • control flow และ if มักเพิ่มความซับซ้อนของโค้ดและโอกาสเกิดบั๊กได้ง่าย
    • โครงสร้างที่อ่านง่ายคือการรวมตรรกะการแตกแขนงที่ซับซ้อนไว้ในฟังก์ชันระดับบนเพียงแห่งเดียว แล้วมอบหมายงานจริงให้ฟังก์ชันระดับล่างที่เป็นเส้นตรง
    • เมื่อ control flow ที่ซับซ้อนถูกรวมอยู่ในฟังก์ชันเดียวภายในหน้าจอเดียว จะสังเกตเห็นเงื่อนไขซ้ำและเงื่อนไขที่ตายแล้วได้ง่ายกว่าการกระจายไปทั่วทั้งไฟล์
  • การรีแฟกเตอร์แบบ “dissolving enum” มีประโยชน์เมื่อการสร้าง enum และการ match ทำซ้ำการแตกแขนงเดียวกัน
    • หาก f() สร้าง E::Foo หรือ E::Bar ตามเงื่อนไข แล้ว g() ก็ match อีกครั้งเพื่อเรียก foo() หรือ bar() การแตกแขนงจะปรากฏสองครั้ง
    • เมื่อดึงเงื่อนไขขึ้นไป จะกลายเป็นรูปแบบที่ main() เรียก foo(x) หรือ bar(y) โดยตรงด้วยเงื่อนไขเดียวกัน

ย้าย for ลงไปยังการดำเนินการแบบแบตช์

  • ในแนวคิดแบบ data-oriented โปรแกรมมักจัดการกับกลุ่มอ็อบเจกต์หลายรายการ และ hot path ก็มักร้อนขึ้นเพราะต้องประมวลผล entity จำนวนมาก
    • การนำแนวคิดเรื่อง แบตช์ ของอ็อบเจกต์เข้ามา และให้เวอร์ชันสเกลาร์เป็นกรณีพิเศษของเวอร์ชันแบตช์ เป็นวิธีที่มีประโยชน์
    • frobnicate_batch(walruses) เป็นรูปแบบที่ดีกว่า for walrus in walruses { frobnicate(walrus) }
  • ประโยชน์หลักของการประมวลผลแบบแบตช์คือ ประสิทธิภาพ
    • เมื่อจัดการหลายเป้าหมายพร้อมกัน จะกระจายต้นทุนเริ่มต้นได้
    • สามารถเปลี่ยนลำดับการประมวลผลได้อย่างยืดหยุ่น และลดความจำเป็นที่ต้องประมวลผล entity ตามลำดับเฉพาะ
    • สามารถใช้เทคนิค vectorization หรือ struct-of-array ที่ประมวลผลฟิลด์หนึ่งของทุก entity ก่อน แล้วจึงไปยังฟิลด์อื่นได้
  • ตัวอย่างด้านประสิทธิภาพแบบสุดขั้วเชื่อมโยงไปยัง Vectorized Interpreters Talk
  • ตัวอย่างที่น่าสนใจคือ การคูณพหุนามด้วย FFT
    • วิธีประเมินพหุนามพร้อมกันที่หลายจุดอาจเร็วกว่าการประเมินทีละจุดหลายครั้ง
  • สามารถใช้กฎ if และ for ร่วมกันได้
    • หากเงื่อนไขอยู่นอกลูป ก็ไม่ต้องประเมิน condition ซ้ำไปมา
    • การแตกแขนง ถูกกำจัดออกจาก hot loop
    • เปิดโอกาสให้ทำ vectorization
  • แพตเทิร์นเดียวกันใช้ได้ทั้งในระดับจุลภาคและระดับมหภาค
    • โครงสร้างของ TigerBeetle เป็นวิธีที่ประมวลผลแบตช์ของอ็อบเจกต์พร้อมกันใน data plane เพื่อกระจายต้นทุนการตัดสินใจของ control plane
  • แรงจูงใจหลักของกฎ for คือประสิทธิภาพ แต่ก็ช่วยด้านพลังในการสื่อความได้เช่นกัน
    • jQuery ทำงานกับคอลเลกชันขององค์ประกอบ และเคยประสบความสำเร็จในอดีต
    • ภาษาของปริภูมิเวกเตอร์เชิงนามธรรมอาจเป็นเครื่องมือช่วยคิดที่ดีกว่าชุดสมการแยกตามพิกัดในบางกรณี

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

 
GN⁺ 2025-05-18
ความคิดเห็นจาก Hacker News
  • โมเดลความคิดแปลก ๆ ของผมคือ มีต้นไม้ของสถานะที่เป็นไปได้และโฟลว์ของโปรแกรมอยู่ แล้วคำสั่งเงื่อนไขก็จะตัดกิ่งต้นไม้นั้น
    ควรตัดกิ่งให้เร็วที่สุดเท่าที่ทำได้ เพื่อให้ทำงานกับกิ่งที่น้อยลงเท่านั้น
    สุดท้ายแล้วผมอยากให้ฟังก์ชันโฟกัสอย่างใดอย่างหนึ่ง ระหว่างการเดินไปตามต้นไม้ของโปรแกรม หรือการทำงานจริง

    • มุมมองนี้สอดคล้องดีกับภาพที่เห็นในทฤษฎีภาษาโปรแกรมหรืออรรถศาสตร์แบบก้าวเล็กของแลมบ์ดาแคลคูลัส
      นิพจน์จะถูก “เขียนใหม่” ซ้ำ ๆ ตามกฎการลดรูปแล้วจึงประเมินค่า ตัวอย่างเช่น (1 + 2) + 4 จะกลายเป็น 3 + 4 แล้วกลายเป็น 7
      ตรงนี้มีกฎความสอดคล้องที่กำหนดว่าจะประเมินนิพจน์ย่อยส่วนไหนถัดไป และมีกฎการคำนวณที่เปลี่ยนนิพจน์จริง ๆ เพื่อเปลี่ยนสถานะของโปรแกรม
      ภาษาที่เข้มงวด หรือก็คือภาษาแบบไม่เลื่อนการประเมิน ส่วนใหญ่จะประเมินนิพจน์ย่อยทั้งหมดก่อนนิพจน์แม่ แต่โครงสร้างพิเศษอย่างเงื่อนไขและลูปไม่สิ้นสุดเป็นข้อยกเว้น
      ในเงื่อนไข กฎการคำนวณจะถูกใช้ก่อนที่กฎความสอดคล้องจะสั่งให้ประเมินนิพจน์ย่อยทั้งหมด จึงเป็นการตัดกิ่งต้นไม้นิพจน์ตามตัวอักษร
      [1]: แนะนำหนังสือ Benjamin C. Pierce, Types and Programming Languages
    • โมเดลความคิดของผมคือปรับให้เข้ากับโลกที่เป็นรูปธรรมซึ่งโค้ดที่กำลังเขียนอยู่ตั้งอยู่
      ต้องดูคุณลักษณะของโดเมน แพตเทิร์นของ codebase เดิม โค้ดอยู่ในขั้นตอนไหนของ data pipeline คุณลักษณะด้านประสิทธิภาพ ฯลฯ
      เมื่อก่อนผมเคยพยายามสร้างกฎและ heuristic สำหรับการจัดโครงสร้างโค้ดแบบนี้ แต่พอเขียนโค้ดมากพอ ก็ยอมรับว่าระดับ abstraction มันผิด และไม่คุ้มที่จะยึดติดนาน
      การที่การถกเถียงแบบนี้ต้องพึ่งชื่อฟังก์ชันสมมติหรือชื่อตัวแปรตัวอักษรเดียวก็ชวนให้คิดเหมือนกัน เพราะใน “เกาะโค้ด” ที่ไม่มีบริบทภายนอก แทบทุกกฎก็ดูสมเหตุสมผลได้
      ต้องสมมติแบบสะดวก ๆ ว่า g เป็นผู้เรียก h เพียงรายเดียว และจะเป็นอย่างนั้นต่อไป จึงจะพูดได้ว่ากฎนี้เผยให้เห็น branch ที่ตายแล้ว
      ใน codebase จริง โดยปกติมักมีเหตุผลที่ไม่ได้รวม g กับ h เข้าด้วยกันตั้งแต่แรก
    • ถ้าจะโยนโมเดลที่ใกล้เคียงมาอีกอันหนึ่ง คลาสคือคำนาม และฟังก์ชันคือกริยา
    • มันก็ไม่ใช่โมเดลที่แปลกขนาดนั้น และถ้าผลักไปจนสุด จริง ๆ แล้วจะใกล้กับโมเดลการทำงานของ Prolog
  • กฎที่ทั่วไปกว่าคือวาง if ไว้ใกล้แหล่งกำเนิดของอินพุต: https://gieseanw.wordpress.com/2024/06/24/dont-push-ifs-up-p...
    ประเด็นสำคัญคือหาให้เจอว่าจุดนั้นคือจุดไหน รวมถึง entry point ที่เข้ามาจากภายนอกสู่โปรแกรม และข้อมูลที่ดึงมาจากบริการอื่น แล้วปรับแต่งให้สร้างหลักประกันให้มากที่สุดเท่าที่ทำได้ ก่อนจะไปถึง core logic โดยเฉพาะส่วนที่ใช้ทรัพยากรมาก
    ถ้าเป็นไปได้ก็ควร encode หลักประกันนั้นไว้ใน type

    • นี่แทบจะเป็นเรื่องเดียวกับจง parse อย่า validate: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
    • ถ้าอย่างนั้นตอนทำความเข้าใจ core logic จะยิ่งไม่คลุมเครือขึ้นหรือว่าเราสามารถตั้งสมมติฐานอะไรได้บ้าง? คุณชอบวิธีไล่ตาม call chain ทุกเส้นเพื่อตรวจสอบทีละอันมากกว่าหรือ?
  • การคาดเดาหลวม ๆ แบบ “ถ้าในฟังก์ชันมีเงื่อนไข if อยู่ ให้พิจารณาว่าย้ายไปฝั่ง caller ได้ไหม” มีตัวอย่างโต้แย้งมากเกินไป
    ถ้าฟังก์ชันถูกเรียกจาก 37 จุด ต้องเขียน if ซ้ำทุกจุดหรือ?
    ถ้าฟังก์ชันนั้นคือ getaddrinfo หรือ EnterCriticalSection ต้องผลัก if ออกไปให้ผู้ใช้ API จัดการหรือ?
    ผมมองว่าการแปลงแบบนี้คิดได้เฉพาะเมื่อเป็นฟังก์ชันภายในที่ถูกเรียกไม่เกินสองจุด และการตัดสินนั้นอยู่นอกความรับผิดชอบของฟังก์ชัน
    อีกวิธีคือให้ฟังก์ชันทำแค่ if แล้วเรียก helper function สองตัว
    ถ้า caller ต้องยกเงื่อนไขออกไปนอกลูป ก็ใช้ “helper ตีความเงื่อนไข” ระดับต่ำได้ ส่วน caller ที่ไม่ต้องแยกใน/นอกลูป แต่ต้องใช้ if เพียงครั้งเดียว ก็ใช้ฟังก์ชันอำนวยความสะดวกที่ซ่อน if ได้
    แต่ทั้งหมดนี้เป็นเรื่องเพื่อการ optimize และการ optimize ก็มักขัดกับโครงสร้างโปรแกรมที่ดี
    ใน OOP การตัดสินด้วย if ภายใน callee จะปรากฏเป็นmethod dispatch ที่เลือกว่าจะเรียกเมธอดไหน
    เทคนิคการย้าย method dispatch ออกไปนอกลูปก็อาจสวนทางกับ flow ของการออกแบบได้
    เช่น ถ้าจะเติมออบเจ็กต์ canvas ด้วยภาพ raster คงไม่อยากวนผ่าน pixel ของภาพแล้วเรียก canvas.putpixel(x, y, color) หรอก ควรมีเมธอดสำหรับ blit ภาพไปยัง canvas หรือบริเวณสี่เหลี่ยมของมัน

    • ถ้าฟังก์ชันถูกเรียกจาก 37 จุด ในกรณีนี้น่าจะหมายถึงการแยกฟังก์ชันออกเป็นสองฟังก์ชันที่ implement branch จริง/เท็จ แล้วให้แต่ละอันถูกเรียกจาก 21 จุดกับ 16 จุดมากกว่า
    • คำสำคัญตรงนี้คือ consider
      บทความนี้เล็งไปที่ปัญหาการออกแบบที่ค่อนข้างเฉพาะเจาะจง ซึ่งมักเกิดขึ้นโดยเฉพาะตอนใช้สิ่งอย่าง tagged union
    • ถ้าฟังก์ชันถูกเรียกจาก 37 จุด ก็ควร refactor โค้ด แต่ถึงอย่างนั้นคำตอบก็ยังเป็น “แล้วแต่สถานการณ์”
      DRY อาจรู้สึกเหมือนเป็นคำตอบที่ถูก แต่ต้องดูตัวอย่างโค้ดจริงถึงจะตัดสินได้
      ถ้าเป็นฟังก์ชันไลบรารี ตำแหน่งของมันมีความพิเศษ มันอยู่ที่ขอบเขตความเป็นเจ้าของ ข้อมูลข้ามโดเมน และถ้าพูดแบบ DDD ก็คือข้ามbounded context ดังนั้นมันต้องปกป้องพื้นที่ของตัวเอง
      EnterCriticalSection ชี้ให้เห็น code path ที่เหมาะกับการตรวจสอบอย่างเข้มตอนเข้า รวมถึงการตรวจสอบด้วยเงื่อนไข if และควรมองว่าเป็นขอบเขตโดเมน
      ในทางกลับกัน ถ้ากำลังเขียนแอปพลิเคชันแล้วฟังก์ชันทั่วไปของแอปพลิเคชันมีคำสั่ง if อยู่ ก็สามารถผลักออกไปข้างนอกได้อย่างปลอดภัย
      แม้ในไลบรารีหรือส่วนโค้ดสำคัญ การยก if ขึ้นไปไว้ที่ขอบ แทนที่จะอยู่ลึกด้านใน ก็ปลอดภัย
      คำแนะนำว่าให้จัดการโดเมนของตัวเอง อย่าไปเรียกร้องจากโดเมนของคนอื่น และภายในโดเมนนั้นให้ย้าย control flow ไปที่ขอบ ฟังดูสมเหตุสมผล
      แน่นอนว่า idiom ก็เป็นแค่ idiom และในโลกจริงต้องให้คนที่เข้าใจบริบทและตัดสินอย่างมีเหตุผลเป็นผู้ประเมิน
  • ตัวอย่าง “การรีแฟกเตอร์แบบแยก enum” โดยแก่นแล้วคือ พหุสัณฐาน (polymorphism)
    สามารถเปลี่ยน match ให้เป็นการเรียกเมธอดแบบพหุสัณฐานบน enum ได้
    เป้าหมายคือการแยกจุดที่สร้างการแยกกรณีออกจากจุดที่มี if แรกและเรียกใช้ foo หรือ bar ตามนั้น
    การแยกกรณีจะถูกพกพาไปกับอ็อบเจกต์ ซึ่งในที่นี้คือค่า enum หรือ closure และไม่จำเป็นต้องทำซ้ำอีกที่จุดเรียก
    กล่าวคือ เมื่อการแยกกรณีเปลี่ยน ก็เปลี่ยนเฉพาะจุดที่สร้างการแยกนั้น และไม่ต้องเปลี่ยนจุดต่าง ๆ ที่กระตุ้นพฤติกรรมตามแต่ละกรณี
    แต่ก็มี trade-off อยู่ การเห็นกรณีแต่ละแบบที่ต้องพิจารณาโดยตรง ณ จุดที่พฤติกรรมถูกเรียกใช้อาจช่วยได้ แต่แลกกับการเพิ่ม dependency ระดับโค้ดต่อรายการกรณีแต่ละแบบ

  • ตัวสแกนความซับซ้อนของโค้ด สุดท้ายแล้วบังคับให้ดัน if ลงไปข้างล่าง แต่บทความแนะนำตรงกันข้าม
    เมื่อยก if ขึ้นไปข้างบน control flow มักถูกรวมศูนย์ไว้ในฟังก์ชันเดียว ฟังก์ชันนั้นจะมีลอจิกแตกแขนงที่ซับซ้อน แต่ตัวงานจริงถูกมอบหมายให้ subroutine ด้านล่างที่เป็นเส้นตรง
    https://docs.sonarsource.com/sonarqube-server/latest/user-gu...

    • ทางแก้คือ แยกการตัดสินใจออกจากการปฏิบัติ และแนวคิดนี้ได้มาจาก Bertrand Meyer
      if (weShouldDoThis()) { doThis(); }
      นี่เป็นส่วนเสริมหรือเป็นส่วนหนึ่งของ functional core และ imperative shell
      ถ้าแยกการตรวจสอบออกมาไว้ต่างหาก ก็ทดสอบได้ง่ายขึ้น และถ้ากังวลเรื่องความซับซ้อน ก็แยกแต่ละ clause ในการตรวจสอบออกเป็นฟังก์ชันได้
    • รายงานจาก code scanner ไม่ควรรับไว้ราวกับเป็นพระคัมภีร์ แต่ควรตั้งข้อสงสัย
      โดยเฉพาะ Sonar รายงาน “code smell” ไม่ใช่บั๊กจริง
      การแก้รายการที่ “ไม่ใช่บั๊ก” แบบนี้ทำให้ความเสี่ยงที่จะเกิดข้อผิดพลาดใหม่เพิ่มจาก 0 เป็นค่าที่มากกว่า 0 และอาจเปลืองเวลานักพัฒนาที่ควรใช้จัดการปัญหา production จริง
    • จากประสบการณ์ของผม เรื่องนี้มักเป็น local optimum
      คำว่า “local” หมายถึงมันเป็นเช่นนั้นแค่จนกว่าข้อกำหนดจะเปลี่ยน หรือพบกรณียกเว้นจนต้องมีการแตกแขนงนอกลูปด้วย
      ถ้ามีการแตกแขนงทั้งในและนอกลูป การให้เหตุผลจะยากขึ้น
      ถ้าค่อนข้างมั่นใจว่าเงื่อนไขจะมีผลเฉพาะในลูปเท่านั้น จะวางไว้ตรงนั้นก็ได้
      แต่ถ้าไม่ยากที่จะจินตนาการถึงข้อกำหนดที่ต้องแตกแขนงนอกลูปด้วย การออกแบบโครงสร้างเผื่อไว้ตั้งแต่แรกอาจดีกว่า
      โค้ดอาจยืดยาวขึ้น แต่ตามอ่านง่ายกว่า และโอกาสที่จะกลายเป็นสปาเกตตีในภายหลังก็ลดลง
      นี่คือเหตุผลที่ผมเลิกใช้ Haskell Haskell ทำให้อยากเขียนตรรกะที่กระชับที่สุดและเป็น “local optimum” แต่สิ่งนั้นเอนเอียงไปทางการแสดงตัวตรรกะเอง มากกว่าความตั้งใจของตรรกะ เมื่อข้อกำหนดเปลี่ยนเล็กน้อย อาจต้องคลี่แก้อย่างน่าสยดสยอง
    • ตั้งแต่เห็นมันบ่นใส่ฟังก์ชันขนาดใหญ่ที่อ่านง่ายมาก ผมก็ไม่ชอบ ตัวสแกนความซับซ้อนของโค้ด มาโดยตลอด
      การมีลอจิกอยู่ในที่เดียวอ่านง่ายกว่ามาก และควรค่อยแยกก็ต่อเมื่อรายละเอียดเริ่มทำให้มองภาพใหญ่ไม่ออก
    • เมื่อวานในเธรดเรื่อง LLM มีคนถามว่า “มีเครื่องมือไม่น่าเชื่อถืออะไรอีกที่ผู้คนยอมรับในการเขียนโค้ด” ตอนนี้ได้คำตอบแล้ว
  • บางครั้งผมชอบให้ลอจิกเงื่อนไขอยู่ในฟังก์ชันที่ถูกเรียก เพราะมันช่วยกันไม่ให้ผู้เรียกเผลอทำงานใน ลำดับที่ผิด
    เช่น ถ้าต้องการสร้าง operation แบบ idempotent ก็อาจตรวจสอบก่อนว่างานนั้นเสร็จไปแล้วหรือยัง ถ้ายังก็ทำงานนั้น
    ถ้าดันเงื่อนไขนั้นออกไปนอกผู้เรียก ผู้เรียกทุกจุดของฟังก์ชันจะต้องตรวจเองว่าเรียกอย่างถูกต้องเพื่อให้ได้หลักประกันเรื่อง idempotency และเราไม่สามารถ abstract หลักประกันนั้นให้ได้
    บางกรณีต้องรันชุดการตรวจสอบก่อนทำงานบางอย่างภายใน database transaction ด้วย ถ้าจะใช้ปรัชญานี้แล้วอยากให้การตรวจสอบยังอยู่ภายในขอบเขต transaction ควรทำอย่างไร?

    • อาจเขียนฟังก์ชันที่ไม่มีการตรวจสอบ แล้วมี wrapper function ที่ทำเฉพาะการตรวจสอบและเรียกฟังก์ชันภายในก็ได้
    • จริง ๆ คำตอบอยู่ในคำถามแล้ว
      ถ้าดันเงื่อนไขออกไปนอกผู้เรียก ฟังก์ชันนั้นก็ไม่เป็น idempotent อีกต่อไป ดังนั้นย่อมให้หลักประกันไม่ได้
      แต่ถ้าฟังก์ชันเดี่ยว ๆ ต้อง implement การจัดการสถานะเพื่อให้ idempotency ได้ โครงสร้างนั้นก็น่าจะค่อนข้างแปลก และดูเหมือนมีลอจิกมากเกินไปเกิดขึ้นในฟังก์ชันเดียว
      โค้ดแบบ idempotent โดยทั่วไปแบ่งเป็นสองกลุ่ม
      กลุ่มหนึ่งคือกรณีที่ data model และ operation ที่ทำมีความเป็น idempotent โดยเนื้อแท้ อาจเป็น operation ไร้สถานะ หรือ operation สไตล์ PUT ที่ข้อมูลอินพุตมีสถานะทั้งหมดที่ต้องบันทึกอยู่แล้ว
      อีกกลุ่มคือ business operation ที่ซับซ้อนกว่า ซึ่งสร้าง abstraction แบบ idempotent ด้วยการให้ abstraction ของการ apply แบบ atomic ที่ทำ rollback หรือรับประกันว่า partial failure จะไม่ทำให้สถานะเสีย
      ในกรณีแรก ไม่จำเป็นต้องตรวจลำดับการทำงาน เพราะมันเป็น idempotent โดยเนื้อแท้ ก็แค่ทำซ้ำได้เลย
      ในกรณีที่สอง ใช้ abstraction ง่าย ๆ ไม่ได้ ต้องบันทึกงานที่ต้องการทำ รับประกันให้งานนั้นสำเร็จหรือ failed แล้วทำให้สถานะสำเร็จหรือ failed นั้นคงอยู่ถาวร
      ลอจิกประเภทนั้นไม่ใช่สิ่งที่ควรใส่ไว้ในฟังก์ชันเดียวแล้วนำไป compose กับ operation อื่น
  • คำแนะนำเหล่านี้มีจุดยืนแรงมาก จึงไม่ควรปฏิบัติเหมือนเป็นกฎจากประสบการณ์
    ผมมองว่าแทบไม่มีกฎจากประสบการณ์อยู่ในนี้เลย ถ้าจะฝืนสร้างขึ้นมา ก็น่าจะพูดกลับกัน
    if ควรถูกดันลงไปข้างล่างเพราะ DRY
    ถ้าประสิทธิภาพเอื้อ ควรพิจารณายก for ขึ้นไปข้างบน เพราะจะสามารถใช้ filter/map/reduce และการประกอบฟังก์ชัน เพื่อเลือกว่าจะนำพฤติกรรมใดไปใช้กับอ็อบเจกต์ใด และโดยเนื้อแท้ก็ทำให้โค้ดเป็น เวกเตอร์ไลซ์ ได้

    • เหมือนอาจสลับชื่อกัน หรือเหตุผลที่ยกมาไม่สนับสนุนข้อสรุป
      การดัน if ลงไปข้างล่างมักขัดขวางการเวกเตอร์ไลซ์
      กรณีที่บทความพูดถึงคือกรณีที่ไม่ DRY โดยเฉพาะเมื่อ type ถูก tag ไว้ภายใน ทำให้การแตกแขนงคล้าย ๆ กันต้องแพร่กระจายไปยังฟังก์ชันจำนวนมากที่อยู่ลึกลงไปใน stack
  • ผมไม่มั่นใจว่านี่เป็นกฎ “ที่ดี” ที่ควรทำตาม
    บางครั้งอาจใช่ แต่ขึ้นกับ บริบท มากเกินไปจนสรุปได้ยาก
    ให้ความรู้สึกคล้ายกฎ “i มาก่อน e ยกเว้นหลัง c” มีข้อยกเว้นมากจนแทบไม่ต่างจากไม่มีกฎเลย

  • ได้เวอร์ชันที่คล้ายกันนี้มาจาก 99 Bottles of OOP ของ Sandi Metz
    โดยรวมแล้วไม่ใช่สไตล์ของผม แต่เวลาเจอโค้ดเบสที่ส่ง flag จำนวนมากลงไปลึกหลายชั้น ประเด็นที่ว่าให้ย้ายทางแยกของลอจิกขึ้นไปไว้ด้านบนของ call stack นั้นน่าเชื่อถือมาก
    https://sandimetz.com/99bottles

    • นึกถึง The Wrong Abstraction ของผู้เขียนคนเดียวกันขึ้นมาทันที
      การเอา branching เข้าไปไว้ในลูป for คือ abstraction ที่บอกว่า “ลูป for คือกฎ และ branching คือพฤติกรรม”
      แต่บ่อยมากที่ requirement ใหม่มาทำลาย abstraction นั้น
      จากนั้นก็ต้องอ้อม และโค้ดที่ได้ก็จะมี abstraction ที่ใช้ได้ในบางกรณีแต่ใช้ไม่ได้ในบางกรณี หรือไม่ก็ยัดพารามิเตอร์จำนวนมากเข้าไปใน abstraction เพื่อให้ใช้ได้ทุกที่จนตามอ่านยาก
      ถ้าไม่ได้สร้าง abstraction นั้นตั้งแต่แรก โค้ดที่ได้อาจแก้ไขและทำความเข้าใจได้ง่ายกว่า
      https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
  • เพื่อความอ่านง่ายของโค้ด การดันทุกอย่างลงไปข้างล่างมักจะดีกว่า
    printInvoice(invoice, options) ดีกว่า if(printerReady){ if(printerHasInk){ if(printerHasPaper){ if(invoiceFormatIsPortrait){ ... }}}} มาก
    ลูปก็เช่นกัน printInvoices(invoices) ดีกว่า for(invoice of invoices){ printInvoice(invoice) }
    สุดท้ายแล้วความอ่านง่ายของโค้ดสำคัญมาก แต่ encapsulation สำคัญกว่า จึงต้องผสมทั้งสองอย่างให้เหมาะสม

    • ฟังก์ชัน printInvoice ควรพิมพ์ใบแจ้งหนี้
      ถ้าหนึ่งในเงื่อนไขที่ชื่อระบุไว้เป็นเท็จจนพิมพ์ใบแจ้งหนี้ไม่ได้ จะเกิดอะไรขึ้น?
      อาจโยน exception หรือคืนค่า sentinel หรือชนิด error กลับมาได้ แต่ในกรณีนั้นสิ่งที่ควรทำต่อไม่ได้ชัดเจนทันที
      โดยเฉพาะในภาษาอย่าง Java หรือ C++ ที่มักไม่ชอบใช้ exception กับ control flow ทั่วไป และ error แบบ monad ก็ไม่แพร่หลาย การจัดโครงสร้างให้ใกล้กับสไตล์ที่สองอาจดีกว่า
      อย่างไรก็ตาม ถ้ารูปแบบแนวตั้งไม่ได้หมายถึงข้อผิดพลาด ก็ควรให้เครื่องพิมพ์ใบแจ้งหนี้เป็นฝ่ายจัดการ
      encapsulation ดูเหมือนจะเป็นเครื่องมือหลัก ๆ เพื่อความอ่านง่ายของโค้ดในระยะยาว, การ refactor และการเปลี่ยนแปลงเฉพาะจุด, รวมถึงความสามารถในการสนใจเฉพาะ object ในบริเวณนั้นแล้วอนุมานพฤติกรรมโดยรวมได้
      ดังนั้นการเอาความอ่านง่ายกับ encapsulation มาเทียบกันแล้วมองว่าอย่างใดอย่างหนึ่งสำคัญกว่าจึงรู้สึกเหมือนเป็น category error
    • การบอกว่า “ดันทุกอย่างลงไปข้างล่างเพื่อเพิ่มความอ่านง่าย” แล้วแสดง arrow anti-pattern นั้นไม่ค่อยดี
      ควรทำแบบนี้แทน
      if(!printerReady){ return; }
      if(!printerHasInk){ return; }
      if(!printerHasPaper){ return; }
      if(!invoiceFormatIsPortrait){ return; }
      อ่านง่ายกว่าลูกศรที่ยิ่งขยายใหญ่ขึ้นมาก
      แต่การเอาลูปไปไว้ในฟังก์ชันของมันเอง และจัดการสมมติฐานอื่น ๆ ไว้เรียบร้อยแล้วนั้นเป็นเรื่องดี
    • นี่อาจเป็นไดรเวอร์เครื่องพิมพ์บน PC หรืออาจเป็นวงจรภายในเครื่องพิมพ์ก็ได้ ดังนั้นแต่ละคนอาจมีความเห็นต่างกัน
      ตอนกระดาษหมด ตัวเครื่องพิมพ์เองต้องไม่ปล่อยให้ล้อหมุนฟรีเด็ดขาด ผมจะเอาการตรวจสอบนั้นไว้ในฟังก์ชัน
    • ถ้าเป็นฝั่ง Elixir ผมน่าจะตั้งชื่อฟังก์ชันนั้นประมาณ maybe_print_invoice ซึ่งผมชอบแบบนี้มากกว่า