2 คะแนน โดย GN⁺ 2025-03-12 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แม้โค้ดจะยอดเยี่ยมในเชิงตรรกะ ก็อาจอ่านนาน ๆ ได้ยาก บทความนี้มองหาความเหนื่อยล้านั้นจาก ความซับซ้อนที่มองเห็นได้ด้วยตา
  • Halstead Complexity Metrics นับจำนวนตัวดำเนินการและตัวถูกดำเนินการเพื่อคำนวณ Volume และ Difficulty และมองว่าการเขียนที่มีตัวแปรกับตัวดำเนินการมากกว่า แม้ทำงานเหมือนกัน ก็ยากกว่า
  • Cognitive Complexity ของ SonarSource ประเมินภาระจากเงื่อนไข ลูป การจัดการข้อยกเว้น การผสมตัวดำเนินการเชิงตรรกะ รีเคอร์ชัน และ goto โดยเน้นที่ไวยากรณ์ย่อ การขัดจังหวะโฟลว์เชิงเส้น และโฟลว์ควบคุมที่ซ้อนกัน
  • ในแง่ตัวแปร variable shadowing, ชื่อที่คล้ายกัน, ช่วงอายุของตัวแปรที่ยาว และรูปแบบการใช้งานที่ไม่คุ้นเคย ทำให้ผู้อ่านต้องเสียต้นทุนมากขึ้นในการติดตามการไหลของข้อมูล
  • ฟังก์ชันเล็ก ๆ, รูปแบบที่คุ้นเคย, การจัดกลุ่มเชนยาว ๆ, เงื่อนไขที่เรียบง่าย, การใช้ goto อย่างจำกัด, การซ้อนที่ตื้น, ชื่อตัวแปรที่แยกแยะได้ และช่วงอายุของตัวแปรที่สั้น กลายเป็นเกณฑ์ด้าน readability ที่ใช้ได้ข้ามภาษาและรูปแบบการจัดฟอร์แมต

ข้อจำกัดและเป้าหมายของตัวชี้วัด readability ของโค้ด

  • readability ของโค้ดไม่มี ตัวชี้วัดเดี่ยวที่ใช้กันแพร่หลายและเป็นที่เห็นพ้องกัน
  • แหล่งอ้างอิงที่พอใช้ได้มักเป็นงานวิชาการที่ไม่ได้ใช้กันแพร่หลายในโลกจริง หรือใกล้เคียงกับความคิดเห็น จึงจำเป็นต้องมีเครื่องมืออภิปรายที่เป็นรูปธรรมกว่าเพื่อใช้ได้ทันทีในการ code review
  • เป้าหมายไม่ใช่การประดิษฐ์ตัวชี้วัดใหม่ แต่เป็นการรวบรวม รูปแบบทางสายตา ที่ใครก็ใช้ได้เมื่อพูดว่าโค้ดอ่านง่ายหรือไม่
  • สิ่งที่นำมาพิจารณาคือการวัดหรือแนวคิดที่เข้าเงื่อนไขต่อไปนี้
    • ใช้กับชิ้นส่วนซอร์สโค้ดหรือฟังก์ชันเดี่ยวได้
    • ไม่ได้โฟกัสเฉพาะ ความซับซ้อนหลัก ที่แยกออกจากอัลกอริทึมของการ implement เองได้ยาก เช่น Cyclomatic Complexity
    • ไม่ได้หยุดอยู่แค่องค์ประกอบสไตล์ผิวเผิน เช่น ความยาวชื่อตัวแปร ช่องว่าง การเยื้อง หรือการวางวงเล็บ

Halstead Complexity Metrics

  • Maurice Halstead เสนอ Halstead Complexity Metrics ในช่วงปลายทศวรรษ 1970 เพื่อสร้างค่าการวัดเชิงประจักษ์ของซอร์สโค้ด
  • ตัวชี้วัดนี้ใช้ได้ข้ามภาษาและแพลตฟอร์ม และโฟกัสที่รูปแบบการเขียนโค้ดมากกว่าอัลกอริทึมที่ถูก implement เอง
  • ค่าการวัดหลักคือจำนวน 4 ค่า โดยอิงจากตัวดำเนินการและตัวถูกดำเนินการ
    • จำนวนตัวดำเนินการที่ไม่ซ้ำ n1
    • จำนวนตัวถูกดำเนินการที่ไม่ซ้ำ n2
    • จำนวนตัวดำเนินการทั้งหมด N1
    • จำนวนตัวถูกดำเนินการทั้งหมด N2
  • จากสิ่งเหล่านี้ Halstead สร้างตัวชี้วัดที่เกี่ยวข้อง เช่น length, volume, difficulty และยังพยายามอนุมานตัวเลขสำหรับคาดการณ์จำนวนบั๊กใน implementation ด้วย
  • โดยสัญชาตญาณ ยิ่งมีตัวดำเนินการมาก ก็ยิ่งต้องพิจารณาปฏิสัมพันธ์ที่เป็นไปได้มากขึ้น และยิ่งมีตัวถูกดำเนินการมาก ก็ยิ่งเข้าใจความเป็นไปได้ของการไหลของข้อมูลได้ยากขึ้น
  • ตัวอย่าง JavaScript

    • แม้จะเป็นฟังก์ชันตรวจเลขคู่คี่แบบเดียวกัน implementation แบบง่ายที่ใช้ if และ return มีตัวดำเนินการและตัวถูกดำเนินการน้อยกว่า
    • ตัวดำเนินการที่ไม่ซ้ำ 4 ตัว, ตัวดำเนินการทั้งหมด 7 ตัว
    • ตัวถูกดำเนินการที่ไม่ซ้ำ 5 ตัว, ตัวถูกดำเนินการทั้งหมด 6 ตัว
    • Volume 33.30, Difficulty 2.50
    • implementation ที่ใช้อาร์เรย์, Number, นิพจน์เปรียบเทียบ และดัชนี มีตัวดำเนินการและตัวถูกดำเนินการมากกว่า
    • ตัวดำเนินการที่ไม่ซ้ำ 7 ตัว, ตัวดำเนินการทั้งหมด 10 ตัว
    • ตัวถูกดำเนินการที่ไม่ซ้ำ 9 ตัว, ตัวถูกดำเนินการทั้งหมด 12 ตัว
    • Volume 71.35, Difficulty 3.75
    • implementation แรกดูด้วยตาก็ง่ายกว่า และค่า Volume กับ Difficulty ของ Halstead ก็สนับสนุนเรื่องนี้
    • ข้อเสียคือ ในทุกภาษาไม่ได้ชัดเจนเสมอไปว่าอะไรควรถูกมองเป็น operator และ operand ดังนั้นในการวัดควรกำหนดเครื่องมือหรือ implementation เฉพาะแล้วใช้ให้สม่ำเสมอ
  • รูปแบบเชิงปฏิบัติที่ได้จาก Halstead

    • โดยทั่วไป ฟังก์ชันยิ่งเล็กและมีตัวแปรยิ่งน้อย ก็ยิ่งอ่านง่าย
    • ตัวดำเนินการเฉพาะภาษา หรือ syntactic sugar เพิ่มภาระให้ผู้อ่าน จึงควรหลีกเลี่ยงการใช้มากเกินไป
    • หากนำองค์ประกอบเชิง functional เช่น map, reduce, filter, lambda, iterator, comprehension มาเชื่อมเป็นเชนยาว ๆ แม้จะกระชับก็อาจทำให้ readability ลดลง
    • เชนยาวลักษณะนี้อาจพบได้บ่อยกว่าใน JavaScript และ Rust หรือในโค้ด Python ที่ใช้ itertools อย่างลึกซึ้ง

ความยากในการอ่านตาม Cognitive Complexity

  • Cognitive Complexity ที่ SonarSource สร้างขึ้น เป็นตัวชี้วัดเพื่อจับความอ่านยากให้แม่นยำขึ้น
  • แนวคิดหลักของตัวชี้วัดนี้มี 3 ข้อ
    • ไวยากรณ์ย่อที่รวมประโยคลดความยากลง
    • ทุกครั้งที่ออกนอกโฟลว์เชิงเส้น ความยากจะเพิ่มขึ้น
    • โฟลว์ควบคุมที่ซ้อนกันเพิ่มความยาก
  • มีคำวิจารณ์ว่าชื่อฟังดูเหมือนเป็นตัวชี้วัดเชิงวิทยาศาสตร์หรือเชิงวัตถุวิสัย แต่ในทางปฏิบัติอาจมองได้ว่าเป็น heuristic ที่ได้ผล
  • ปัญหาความหนาแน่นของไวยากรณ์ย่อ

    • ไวยากรณ์ย่ออย่าง MyObj myObj = a?.myObj; สั้นกว่าและใช้เวลาอ่านน้อยกว่าแบบ if (a != null) { myObj = a.myObj; }
    • แต่โค้ดสองแบบอาจไม่ได้เหมือนกันทุกประการจริง ๆ
    • แบบแรกทำให้ myObj เป็น a.myObj หรือ null
    • แบบที่สองทำให้ myObj เป็น a.myObj หรือ undefined
    • แม้ภาษาที่ตรวจชนิดเข้มงวดอย่าง TypeScript หรือ Rust จะลดความเป็นไปได้ที่จะพลาดได้ แต่ก็ไม่ได้รับประกันว่าจะจัดการทุกกรณีได้ถูกต้อง
    • หากการรองรับการตรวจชนิดอ่อนกว่า เช่น JavaScript ทั่วไป โอกาสที่ corner case แบบนี้จะไม่ถูกจัดการก็สูงขึ้น
    • ไวยากรณ์ย่ออาจเขียนง่ายและอ่านง่าย แต่มี trade-off ระหว่าง ความกระชับกับความหนาแน่น
  • องค์ประกอบที่ทำลายโฟลว์เชิงเส้น

    • โค้ดเชิงเส้นที่ไม่มีเงื่อนไขอ่านแบบกวาดตาได้ง่ายกว่าโค้ดที่มีเงื่อนไข
    • Cognitive Complexity มองว่าไม่ใช่แค่เงื่อนไข ลูป และ goto แต่รวมถึง conditional macro, try/except, ลำดับตัวดำเนินการเชิงตรรกะ และรีเคอร์ชัน ล้วนเป็นองค์ประกอบที่เพิ่มความยาก
    • switch ถูกคำนวณเป็นกลุ่มเดียว แต่เชน else-if ถูกมองว่ายากขึ้นทุกครั้งที่มี else-if เพิ่ม
    • เพราะ else-if สามารถมีการเปรียบเทียบตั้งแต่สองอย่างขึ้นไปในแต่ละ branch
    • อย่างไรก็ตาม fall-through ของ switch และ break ที่ขาดหายไปก็เพิ่มความยากในการอ่านได้เช่นกัน
    • กรณีที่ต่อกันด้วยตัวดำเนินการเชิงตรรกะตัวเดิมในนิพจน์เงื่อนไข กับกรณีที่ผสม &&, ||, ! จะให้ความยากต่างกัน
    • debug || verbose || consoleMode เป็นนิพจน์เงื่อนไขที่เรียบง่าย
    • debug || (verbose && consoleMode) อ่านยากขึ้นเพราะมีการผสมตัวดำเนินการ
    • debug || !(verbose && consoleMode) ซับซ้อนขึ้นอีกเพราะรวมการปฏิเสธด้วย
  • การจัดการข้อยกเว้นและ goto

    • try/catch เพิ่มความยากใน Cognitive Complexity แต่การมีหลายบล็อก catch ไม่ได้ถูกมองว่ายากกว่าการมี catch เดียว และ try กับ finally จะถูกละเลย
    • การ throw exception เองก็สร้างต้นทุนในการอ่านได้
    • เมื่อการจัดการ exception ข้ามขอบเขตฟังก์ชัน ความซับซ้อนของฟังก์ชันที่เกี่ยวข้องจะพันกัน
    • ผู้อ่านต้องค้นหาว่า exception นั้นถูกจับที่ไหน
    • โดยทั่วไป goto ถูกนับเป็นองค์ประกอบที่เพิ่มความยาก
    • แต่รูปแบบ goto out หรือ goto done ที่ปล่อยทรัพยากรแล้วออกจากฟังก์ชันในเงื่อนไขข้อผิดพลาด ผู้เชี่ยวชาญบางส่วนมองว่าอาจมีประโยชน์
    • ในทางกลับกัน goto ที่ข้ามขอบเขตลูปในแบบที่ใช้ continue หรือ break แทนไม่ได้ สร้างภาระในการอ่านสูง เพราะผู้อ่านต้องประกอบโฟลว์ควบคุมใหม่

การซ้อนและรูปทรงของฟังก์ชัน

  • หากเงื่อนไขเองก็อ่านยาก เงื่อนไขที่ซ้อนกันก็ยิ่งอ่านยากกว่า
  • Cognitive Complexity ให้ความยากเพิ่มตามระดับการซ้อน นอกเหนือจากคะแนนของเงื่อนไขและลูปเอง
  • แนวคิดนี้ยังถูกเรียกด้วยชื่ออย่าง “Level of Indentation” หรือ “Bumpy Road”
  • เมื่อการซ้อนเกิน 2 ระดับ จะอ่านยากเป็นพิเศษ และโค้ดที่ลดการซ้อนด้วย early return จะอ่านได้ราบกว่า
  • ตัวชี้วัดนี้ไม่ได้สะท้อนความยาวฟังก์ชันโดยตรง แต่ถ้าเงื่อนไขอื่นเท่ากัน ฟังก์ชันยาวย่อมใช้แรงอ่านมากกว่าฟังก์ชันสั้น

ชื่อตัวแปร ช่วงอายุ และรูปแบบที่คุ้นเคย

  • ชื่อที่แยกแยะได้และสื่อความหมาย

    • ชื่อที่สื่อความหมายสำคัญต่อการเข้าใจว่าโค้ดพยายามทำอะไร ส่วนชื่อที่ซ้ำซ้อนหรือเหมือนรหัสลับให้ผลตรงข้าม
    • variable shadowing เป็นเรื่องอันตราย
    • ควรหลีกเลี่ยงสถานการณ์ที่ผู้อ่านต้องไล่กฎ scope เพื่อแยกแยะว่าตัวแปรใดถูกใช้
    • ควรหลีกเลี่ยง identifier ที่ดูคล้ายกันด้วย
    • ชื่อที่สับสนได้ง่ายด้วยสายตา เช่น i กับ j, item กับ items อาจนำไปสู่ความผิดพลาด
    • โค้ดที่ใช้รูปแปรหลายแบบของชื่อตัวแปรเดียวกันในฟังก์ชันเดียว เช่น node, _node, thisNode เป็นรูปแบบที่ทำให้เกิดบั๊กได้ง่าย
  • ช่วงอายุของตัวแปรที่สั้น

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

    • สไตล์ functional programming ทำให้ช่วงอายุตัวแปรสั้น แต่เชนที่ยาวเกินไปหรือ callback ที่ซ้อนกันอาจสร้างภาระในการอ่านได้
    • การแบ่งเชนฟังก์ชันยาว ๆ เป็นกลุ่มเล็ก ๆ และใช้ตัวแปรกลางหรือ helper function ที่ตั้งชื่อดี จะช่วยลดภาระทางปัญญาของผู้อ่านได้
    • เวอร์ชันที่ใช้ตัวแปรกลางอาจมีประสิทธิภาพน้อยลงเล็กน้อย
    • ตราบใดที่เครื่องมือด้าน performance ไม่ได้บอกว่าบรรทัดนั้นเป็นคอขวดจริง ความต่างด้านประสิทธิภาพเล็กน้อยแบบนี้ไม่สำคัญ
  • การใช้รูปแบบโค้ดที่คุ้นเคยซ้ำ

    • การใช้โค้ดและรูปแบบตัวแปรที่คุ้นเคยซ้ำ ทำให้ผู้อ่านจำ pattern ที่รู้อยู่แล้วได้ จึงอ่านได้เหนื่อยน้อยลง
    • เรื่องนี้สอดคล้องกับ Principle of Least Surprise
    • ภายใน codebase ควรรักษารูปแบบที่เกิดซ้ำ เช่น วิธีเขียนเงื่อนไข ให้สม่ำเสมอ
    • เมื่อต้องออกนอก pattern อาจใช้ชื่อตัวแปรหรือคอมเมนต์เพื่อแสดงความแตกต่างได้
    • หากผลักแนวคิดนี้ไปจนสุด ก็จะนำไปสู่การใช้ template function หรือ generic function เพื่อให้ผู้อ่านไม่ต้องจำ pattern ที่ซ้ำกันใหม่อีกครั้ง

8 รูปแบบทางสายตาที่ช่วยเพิ่ม readability

  • Line/Operator/Operand count: ฟังก์ชันที่เล็กกว่า และตัวแปรกับตัวดำเนินการที่น้อยกว่า อ่านง่ายกว่า
  • Novelty: หลีกเลี่ยงความแปลกใหม่ในรูปทรงฟังก์ชัน ตัวดำเนินการ และ syntactic sugar แล้วใช้ pattern ร่วมของ codebase ซ้ำ
  • Grouping: เชนฟังก์ชันยาว ๆ, iterator, comprehension ควรแบ่งเป็นกลุ่มเชิงตรรกะด้วย helper function หรือตัวแปรกลาง
  • Conditional simplicity: รักษานิพจน์เงื่อนไขให้สั้นที่สุดเท่าที่ทำได้ และในเงื่อนไขเดียวให้เลือกใช้ลำดับของตัวดำเนินการชนิดเดียวกันมากกว่าผสมตัวดำเนินการเชิงตรรกะต่างชนิด
  • Gotos: อย่าใช้ goto เว้นแต่จะทำตาม pattern การจัดการข้อผิดพลาดเฉพาะ และทางเลือกอื่นแย่กว่า
  • Nesting: ลดโลจิกที่ซ้อนกันและการเปลี่ยนระดับเยื้องมาก ๆ ให้น้อยที่สุด และถ้าจำเป็นต้องซ้อนลึก ให้แยกเป็นฟังก์ชันต่างหากแทนที่จะฝังไว้ในฟังก์ชันใหญ่
  • Variable distinction: ใช้ชื่อตัวแปรที่สื่อความหมายและแยกแยะได้ด้วยสายตา และหลีกเลี่ยง variable shadowing
  • Variable liveness: รักษาช่วงอายุตัวแปรให้สั้น และระวังเป็นพิเศษกับช่วงอายุยาวที่ข้ามขอบเขตฟังก์ชัน

ปัญหาที่ปรากฏใน codebase จริง

  • codebase ที่ทำให้เกิดความเหนื่อยล้าทางใจอย่างมาก มี anti-pattern หลายอย่างพร้อมกัน
  • โดยเฉพาะฟังก์ชันยาว การผสมองค์ประกอบของภาษาหลายแบบ และเชนฟังก์ชันจำนวนมากที่ควรแยกออกเป็น helper function
  • ผลคือในฟังก์ชันใหญ่ ความซับซ้อนจากการซ้อน และช่วงอายุตัวแปรที่ยาวเพิ่มขึ้นพร้อมกัน
  • แม้คุณภาพของโค้ดและผู้เขียนจะสูง ก็ยังพบ critical bug อย่างน้อยหนึ่งรายการ
  • หนึ่งในนั้นเป็นบั๊กที่ดูง่าย แต่มีแนวโน้มว่าจะพลาดไปเพราะมันอยู่กลางฟังก์ชันที่ยาวและซับซ้อน จึงยากต่อการให้เหตุผล
  • คนที่มีแนวโน้มจะอ่านโค้ดของตัวเองมากที่สุดในอีกหนึ่งเดือนข้างหน้าคือตัวเราเอง ดังนั้นการเขียนให้อ่านง่ายจึงเชื่อมโยงกับต้นทุนการทำงานจริงโดยตรง

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

 
GN⁺ 2025-03-12
ความคิดเห็นจาก Hacker News
  • บทความบอกว่า สาย map/reduce/filter ที่ยาวหรือมีหลายชุดทำให้ความอ่านง่ายลดลง แต่ประเด็นนี้แทบไม่ได้มีที่มาจากเนื้อหาส่วนอื่นเลย
    ให้ความรู้สึกเหมือนแอบสอดคำบ่นแบบคลาสสิกว่าอะไรที่ไม่คุ้นเคยก็แย่ ทั้งที่พอคุ้นขึ้นมาสักนิด หลายครั้งมันทั้งอ่านและเขียนง่ายกว่าทางเลือกอื่นมาก
    เช่น ผมว่าแทบยากจะยกตัวอย่างโค้ดที่อ่านง่ายกว่า books.filter(book => book.pageCount > 1000).map(book => book.author).distinct()
    ถ้ามองด้วยตัวชี้วัดด้านความซับซ้อน โค้ดแบบนี้ก็ดีกว่าแทบทุกวิธีอื่น และอย่างน้อยก็ควรเรียนรู้ พื้นฐานของการเขียนโปรแกรมเชิงฟังก์ชัน ไว้บ้าง ไม่ถึงขั้นต้องอธิบาย monad แต่ก็ควรคุ้นกับ map และ filter มากพอที่จะไม่ด่ามันแบบเหมารวม

    • น้ำเสียงอาจจะคมเกินความจำเป็น แต่ตัวอย่างที่ประโยคที่ยกมาหมายถึงไม่ใช่ chain สั้น ๆ แบบนั้น
      ผมมองว่าพอเป็น chain ที่ต่อกันราว 5 ครั้ง ก็เริ่มอ่านยากแล้ว และตัวอย่างในบทความก็ยาวประมาณนั้น
      คำว่า “multiple” หมายถึงกรณีที่มี chain ซ้อนกัน หรือชนิดข้อมูลที่กำลังจัดการเปลี่ยนไประหว่างทาง ซึ่งกรณีแบบนี้ทำให้อ่านช้าลง
      องค์ประกอบเชิงฟังก์ชันอาจดูสง่างามได้ แต่ก็ใช้มากเกินไปได้เหมือนกัน
    • ตัวอย่างนั้นเป็นแค่การกรองรายการเดียวที่เรียบง่ายในเชิงแนวคิด
      พอ chain ยาวเกินไป เงื่อนไขซับซ้อนขึ้น และมีหลายรายการ/หลายตัวแปร ก็จะทำความเข้าใจทั้งหมดในครั้งเดียวยากขึ้น
      ในลูปแบบกระบวนวิธี เราสามารถ ตั้งชื่อให้ผลลัพธ์ระหว่างทาง ได้ และชื่อนั้นช่วยให้เราลืมรายละเอียดของขั้นก่อนหน้าไปชั่วคราวเพื่อโฟกัสกับขั้นถัดไปได้
    • SELECT DISTINCT author FROM books WHERE pageCount > 1000;
    • ตัวอย่างนั้นก็โอเค แต่คำบ่นดั้งเดิมน่าจะหมายถึง chain ที่ยาวมาก มากกว่า
      ส่วนตัวผมมักแยก chain ออกเพื่อจะได้ตั้งชื่อให้ผลลัพธ์ระหว่างทาง และใช้ชื่อตัวแปรเหมือนเป็นคอมเมนต์
      var longBooks = books.filter(book => book.pageCount > 1000)
      var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
    • วิธีแก้ด้วย SQL ที่คนยกมาก็ดี แต่ถ้าเป็น Prolog ก็เขียนได้แบบนี้
      ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
      ขึ้นอยู่กับโครงสร้างฐานข้อมูลของ Prolog อาจเขียนให้สั้นกว่านี้ได้อีก
      ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
  • ผมคิดว่าโค้ดที่ดีโดยพื้นฐานแล้วมี มิติที่เป็นเชิงคุณภาพและเชิงวรรณศิลป์ อยู่มากพอสมควร
    โปรแกรมเมอร์หรือคนในแวดวงวิชาการที่คุ้นกับการคิดแบบคณิตศาสตร์และอยากได้คำตอบเชิงปริมาณ มักจะอึดอัดกับเรื่องนี้
    ผมชอบทั้ง Dostoyevsky และ Wodehouse ซึ่งทั้งคู่ยอดเยี่ยมแต่คนละแบบ
    ถึงการเขียนโค้ดจะไม่ได้เปิดกว้างเท่านั้น แต่ผมก็เคยทำงานกับโค้ดเบสที่ดีหลายชุดซึ่งให้ความรู้สึกต่างกันอย่างสิ้นเชิงในเชิงคุณภาพ และบ่อยครั้งก็ต้องใช้เวลาเหมือนเวลาที่เราค่อย ๆ คุ้นกับสำนวนของนักเขียนคนใหม่ กว่าจะจับ “อารมณ์” ของสไตล์ในโค้ดเบสนั้นได้

    • เห็นด้วย 100% หนึ่งในคำชมที่ดีที่สุดที่ผมเคยได้รับเกี่ยวกับการเขียนโปรแกรมคือ “โค้ดอ่านเหมือนเรื่องเล่า
      มันหมายความว่าถ้าอ่านไฟล์จากบนลงล่าง ลำดับของฟังก์ชันจะทำให้เรื่องราวไหลไปอย่างเป็นธรรมชาติ และการเขียนจะเป็นเชิงประกาศราวกับกำลังพูดกับผู้อ่าน
      ผมยึดตามกระบวนทัศน์การเขียนโปรแกรมเชิงฟังก์ชันล้วน และคิดว่าวิธีนี้เข้ากับสไตล์ที่เป็นเรื่องเล่ามากกว่า
      การพึ่งพา/อินพุตของฟังก์ชันถูกจำกัดอยู่แค่อาร์กิวเมนต์หรือฟังก์ชันบริสุทธิ์อื่น ๆ และเอาต์พุตก็อยู่ในชนิดค่าที่คืนกลับเท่านั้น จึงพาผู้อ่านผ่านความซับซ้อนทีละขั้นได้ง่าย
      ต่างจากกระบวนทัศน์อื่นที่มีความซับซ้อนอย่างสถานะที่ซ่อนอยู่ ผมกลับมองว่ากระบวนทัศน์ที่แม่นตรงทางคณิตศาสตร์ที่สุดนี่แหละ กลับเหมาะกับสไตล์แบบเรื่องเล่ามากที่สุด
    • ถ้าต้องใช้เวลาเกิน 5 วินาทีในการอ่านและเข้าใจ เป้าหมายระดับสูง ของฟังก์ชัน ผมถือว่านั่นคือโค้ดที่ไม่ดี
      หน้าตาเป็นแบบไหนไม่สำคัญ ถ้าไม่มีประสบการณ์พัฒนายาวนานแล้วคนยังไม่สามารถเข้าใจได้ภายในเวลาที่สมเหตุสมผลว่าฟังก์ชันนั้นทำอะไร มันก็คือโค้ดที่ไม่ดี
    • แพตเทิร์นไวยากรณ์หลายแบบที่มักถูกมองว่าสง่างาม ในความเป็นจริงกลับให้ความรู้สึกไม่ชัดเจนยิ่งกว่าคณิตศาสตร์เสียอีก
      ตัวอย่างเช่น ternary operator ในบทความ return n % 2 === 0 ? 'Even' : 'Odd'; สำหรับสมองมนุษย์มันเหมือนต้องอ่านย้อนทาง และเหมาะกับคอมไพเลอร์ที่ประมวลผล parse tree มากกว่ามนุษย์
      ถ้าเป็นนักคณิตศาสตร์ที่เป็นมนุษย์จริง ๆ ก็คงเขียนในรูป ฟังก์ชันแบบแยกกรณี ประมาณว่า ถ้า n mod 2 = 0 ก็เป็น 'Even' ไม่อย่างนั้นเป็น 'Odd' ซึ่งชัดเจนกว่ามาก
    • เพราะแบบนี้ code review จึงสำคัญ
      มันช่วยให้สมาชิกใหม่ของทีมเรียนรู้สไตล์ที่สม่ำเสมอ และช่วยให้สไตล์ของทีมโดยรวมคงความสอดคล้องในระดับที่เหมาะสม
      คอมเมนต์เรื่อง .editorconfig ก็อ่านประกอบได้: https://news.ycombinator.com/item?id=43333011
      ช่วยลดการเถียงกันเรื่องรายละเอียดด้านสไตล์ใน pull request
    • หรือบางทีนี่อาจเป็นเหตุผลว่าทำไม Literate Programming ถึงไม่แพร่หลาย
  • บทความนี้ดี แต่ผมคิดว่ามันพลาดประเด็น ความเปลี่ยนแปลงได้ ซึ่งเป็นสิ่งที่ทำให้เหนื่อยล้าทางความคิดมากที่สุดเวลาอ่านโค้ด
    การที่ระหว่างอ่านเมธอด เราสามารถ “ตรึง” ความหมายของตัวแปรไว้ได้เพียงครั้งเดียว แล้วคงมันไว้อย่างนั้นระหว่างอนุมานส่วนที่เหลือ ถือเป็นของขวัญชิ้นใหญ่
    ความเข้าใจต่อเมธอดควรเพิ่มจาก 0% ไป 100% อย่างต่อเนื่อง ไม่ใช่ต้องเริ่มทำความเข้าใจเมธอดใหม่ในหัวอีกครั้ง เพราะเกิดสับสนว่าในรอบใดรอบหนึ่งลูปบอดีได้เปลี่ยน accumulator อย่างไร
    นี่ก็เป็นเหตุผลที่แท้จริงว่าทำไม GOTO ถึงเป็นอันตราย ปัญหาไม่ใช่ว่าการย้าย instruction pointer ในหัวภายในเมธอดนั้นยาก แต่คือเมื่อมี GOTO แล้ว การรู้ สถานะของตัวแปรที่เปลี่ยนแปลงได้ กลายเป็นเรื่องยาก

    • ไม่เห็นด้วย มี พื้นที่ข้อมูล เชิงนามธรรมที่โค้ดกำลังจำลองอยู่ และ instruction pointer ในหัวก็ต้องเคลื่อนที่ไปในพื้นที่นั้น
      ทั้งตัวแปรแบบเปลี่ยนแปลงได้และเปลี่ยนแปลงไม่ได้ อาจช่วยหรือขัดขวางการเคลื่อนที่นั้นก็ได้ ขึ้นอยู่กับว่าโค้ดสอดคล้องกับพื้นที่นั้นอย่างเรียบร้อยแค่ไหน
      ตัวแปร immutable มีข้อได้เปรียบเชิงยุทธวิธีเล็กน้อยตรงที่ไม่ต้องกังวลว่าค่าจะเปลี่ยน หรือเปลี่ยนในแบบที่ทำให้เข้าใจผิด แต่จากประสบการณ์ ข้อดีนี้ยังไม่มากพอจะยกระดับเป็นกฎว่า “ต้องใช้ immutable เสมอ”
      บางครั้งความเปลี่ยนแปลงได้กลับทำให้ถ่ายทอดพื้นที่ข้อมูลนั้นได้สะอาดกว่ามาก
    • ความซับซ้อนโดยรวมไม่ได้มีแค่เรื่องการย้าย instruction pointer จากจุดเริ่มต้นที่รู้แล้ว
      หากมองจากฝั่งที่ถูกเรียกแทนที่จะเป็นจุดเรียก เมื่อใครบางคนกระโดดมาที่บรรทัดใดบรรทัดหนึ่งได้ คุณจะย้อนดูไม่ได้ว่าเกิดอะไรขึ้นก่อนหน้านั้น เพราะมันอาจมาจากที่ไหนก็ได้ จึงไม่ใช่การวิเคราะห์เชิงเฉพาะที่อีกต่อไป แต่ต้องเป็น การวิเคราะห์โปรแกรมทั้งระบบ
      ถ้าความเปลี่ยนแปลงได้คือแหล่งที่มาที่แท้จริงของความซับซ้อนแบบ GOTO งั้น if และลูป for ก็ควรมีปัญหาแบบเดียวกัน
      ผมเห็นด้วยว่าความเปลี่ยนแปลงได้และสถานะสร้างความซับซ้อนโดยตรง แต่ GOTO เป็นอีกหมวดหนึ่งที่อันตรายกว่ามากโดยสิ้นเชิง
  • แพตเทิร์นที่ผมไม่ชอบเป็นการส่วนตัวคือการ return ทันทีใน if แล้วปล่อยที่เหลือเป็นเส้นทางปริยายเริ่มต้น
    if (n % 2 === 0) return "Even"; return "Odd"; สั้นกว่าจริง แต่ผมชอบ if ... return "Even"; else return "Odd"; มากกว่าเยอะ
    เพราะแบบแรกให้ความรู้สึก ไม่สมมาตร ขณะที่ "Even" กับ "Odd" เป็นทางเลือกที่สมมาตรกัน ดังนั้นแบบที่มี else จึงตรงสัญชาตญาณมากกว่า

    • ผมมักจะเขียนเป็น return (n % 2 === 0) ? "Even" : "Odd"; เพราะมี boilerplate น้อยที่สุด เลยอ่านง่ายที่สุด
      ถ้าเป็นภาษาที่มี ternary operator ก็ควรจะอ่านรูปแบบนี้ออกได้ไม่ยาก
    • guard clause / การคืนค่าก่อนกำหนด มักจะย้ายจุดโฟกัสของนักพัฒนาออกจากเส้นทางปกติเดิม ไปสู่การค่อย ๆ จำกัดพฤติกรรมของฟังก์ชันให้แคบลง
      จากประสบการณ์ else สร้างการซ้อนเพิ่ม และทำให้ต้องคอยประเมินเงื่อนไขขอบเขตหรือตัวแปรที่อยู่นอกขอบเขตทันทีของเส้นทางปกติต่อไปง่ายขึ้น คุณต้องแบกบริบททั้งหมดนั้นไว้
    • ส่วนตัวแล้วผมชอบแบบแรกมากกว่า
      เรามองเห็นการคืนค่าได้ในระดับการเยื้องหนึ่งชั้นถัดจากชื่อฟังก์ชัน และถ้าไม่มีการจบก่อนกำหนด ก็ให้ความรู้สึกว่ามีผลลัพธ์ที่รับประกันไว้
      ถ้าการคืนค่าถูกฝังไว้ลึกลงไปกว่านั้น มันจะให้ความรู้สึกแปลก ๆ
    • ผมจะใส่บรรทัดว่างเพื่อให้ return "Odd"; ดูแยกออกจาก if และถ้าภาษาอนุญาตก็จะใส่วงเล็บปีกกาให้บอดีของ if ด้วย
      มีบางกรณีที่ยอมรับ else ได้ แต่ส่วนใหญ่คือเมื่อมี side effect และโดยทั่วไปถ้ารีแฟกเตอร์จนเอาออกได้ก็มักจะชัดเจนขึ้น
      บ่อยครั้งโค้ดที่ซับซ้อนสามารถเปลี่ยนเป็น ลำดับของ guard ที่ค่อย ๆ ออกไปตามลำดับความสำคัญหรือค่าใช้จ่ายในการรัน และช่วยแยกตรรกะจริงของฟังก์ชัน/เมธอดออกจากเงื่อนไขยุติ
    • ความไม่สมมาตรจะชัดเจนเมื่อคุณรีแฟกเตอร์โค้ดไปเป็นสไตล์ continuation/callback หรือการกลายพันธุ์ของโครงสร้างข้อมูลที่ซับซ้อนกว่า
      แบบแรกจะไหลลงด้านล่างแล้วรันชุดคำสั่งชุดที่สอง return เป็นโอเปอเรเตอร์พิเศษที่ตัดการไหลของการควบคุม ดังนั้นการไหลของการควบคุมทั่วไปของแบบแรกจึงไม่ได้ถ่ายทอด ความครบถ้วน ของสองกรณีได้อย่างเหมาะสม
      ใน Rust แบบ idiomatic มักไม่ใช้ return เว้นแต่จะเป็นกรณีพิเศษที่ขัดจังหวะการไหลของการควบคุมของเมธอด และตัวอย่างที่สองก็มักพบได้บ่อยกว่าในรูปที่ไม่มีคำสั่งคืนค่า
      Python ก็เช่นกัน โดยทั่วไปจะคืนค่าก่อนตั้งแต่ต้นเมื่ออาร์กิวเมนต์หรือสถานะไม่ถูกต้อง และใช้การคืนค่าในตำแหน่งท้ายเป็นค่าที่คืนจริง
      ด้วยธรรมเนียมแบบนี้ เมื่อทำลายโครงสร้าง if-else ที่สมบูรณ์ return ที่มีการเยื้องจึงดูเหมือนกรณียกเว้น หากยึดตามธรรมเนียมนี้อย่างสม่ำเสมอ คำสั่ง return จะดูเหมือนส่วนเกินโดยธรรมชาติ ยกเว้นเมื่อใช้เพื่อตัดการไหลของการควบคุม และทำให้เข้าใจธรรมเนียมของ Rust ได้ ในทุกภาษา return คือคำสั่งที่เทียบได้กับ break
  • อาจจะมีแค่ผมคนเดียวก็ได้ แต่บางครั้ง TypeScript ทำให้โค้ดอ่านยาก
    ถ้าคุณคง data model ให้ “เป็นอะตอม” ในระดับหนึ่ง และนักพัฒนาขยันประกาศกับทำเอกสารของ type จริง ๆ มันก็โอเค
    แต่พอ type เริ่มถูกอนุมานมาจาก type อื่นผ่าน utility type และละการระบุ type แบบชัดเจนไปพึ่งการอนุมานของ type เรื่องก็เริ่มพังอย่างรวดเร็ว
    มันยากมากที่จะไล่ตามว่าฟิลด์หนึ่งมาจากไหน เมื่ออยู่ในสแตกการอ้างอิงอ้อมของ type ที่ลึก 4-5 ชั้น บางส่วนเป็นการอนุมาน บางส่วนประกาศชัด บางส่วนเป็น type ที่สร้างต่อ และยังมี alias ของฟิลด์ปนอยู่
    ใน data model ขนาดใหญ่กับ call stack ที่ลึก รูปแบบอย่าง function checkDogs(dogs: Dog[]) { ... } ที่ละ return type ไปนั้นใช้ไม่ได้เลยและน่าหงุดหงิดมาก

    • ผมคิดว่าฟังก์ชันควรระบุ ชนิดผลลัพธ์ ไว้
      เหตุผลหลักคือเพื่อบังคับให้ทุกเส้นทางที่คืนค่าจากฟังก์ชันนั้นสอดคล้องกับชนิดดังกล่าว
      ผมเคยเห็นหลายครั้งที่มีการเพิ่มเงื่อนไขใหม่ แล้วคืน type ที่ต่างจากกิ่งอื่นเล็กน้อยจนเกิด regression
      แต่ผมไม่คิดว่าการใส่ type ให้ตัวแปรประกาศจะมีคุณค่ามากนัก
      ในตัวอย่าง const checkedDoggos = checkDogs([]) นั้นดีแล้ว และปล่อยให้ checkedDoggos สืบทอด type จากฟังก์ชันก็พอ
      ผมกำลังทำงานกับ codebase ที่ linter บังคับให้เขียน const checkedDoggos: DogBreedAndSize[] = checkDogs([]) ซึ่งค่อนข้างตลกและแทบไม่มีค่าอะไร
    • ใน TypeScript แม้บางส่วนของ return type จะถูกอนุมาน ผมก็ยังชอบให้มีข้อมูล type อยู่บ้าง มากกว่ากลับไปเป็น JavaScript ที่ไม่มีข้อมูล type เลย
      ใน JavaScript เรามั่นใจไม่ได้ จึงต้องไล่ขึ้นไล่ลงในสแตกตลอดและคอยเก็บทุกอย่างไว้ในหัว
    • แนวทางของผมคือจะใส่ type ก็ต่อเมื่อ TypeScript compiler ร้องขึ้นมาเท่านั้น
  • ควรระวังกับคำกล่าวที่ว่า “ฟังก์ชันเล็กที่มีตัวแปรน้อยมักอ่านง่ายกว่า”
    ฉันไม่ชอบเวลาการถกเรื่องความอ่านง่ายไปโฟกัสแค่ ความอ่านง่ายระดับจุลภาค เพราะมันทำให้โค้ดถูกซอยย่อยมากเกินไปภายใต้สมมติฐานที่ผิดว่าความอ่านง่ายระดับจุลภาคสำคัญกว่าความอ่านง่ายระดับมหภาค
    การยึดถือแบบนี้ทำให้เกิดโปรแกรมเมอร์ที่เห็นแต่ต้นไม้ไม่เห็นป่า และนำไปสู่โค้ดที่ไม่มีประสิทธิภาพเกินจำเป็นหรือดีบักได้ยาก
    ภาษาตระกูล APL อยู่คนละสุดขั้ว แต่จุดที่เหมาะจริงน่าจะอยู่ตรงกลางสักแห่ง และน่าจะแตกต่างกันมากในแต่ละคน

    • โดยเฉพาะเมื่อมีหลายไฟล์พันกัน จะมี จุดกึ่งกลาง นี้อย่างชัดเจน
      แค่กระโดดไปดู definition ในโค้ดที่ไม่คุ้น 3-4 ครั้งก็เริ่มหนักแล้ว อาจเป็นปัญหาของฉันเอง แต่ก็ยากจะนึกภาพว่าคนส่วนใหญ่จะเก่งกว่าฉันมากนัก
      ในวัฒนธรรม .NET โดยเฉพาะ “clean architecture” ปัญหานี้หนักจนน่าตกใจ เวลาแก้ฟีเจอร์หรือไล่ปัญหา มันกระจายอยู่ใน 4 ชั้นกับ 15 ไฟล์ และบางไฟล์มีแต่คีย์เวิร์ดเกิน 60%
      ฉันไม่รู้ว่าควรขีดเส้นตรงไหน แต่โดยรวมแล้วฉันมักชอบฟังก์ชันยาวฟังก์ชันเดียวที่ยังอ่านต่อเนื่องได้และทำตามคำแนะนำอื่น ๆ มากกว่าโค้ดที่แตกเป็นชิ้นเล็กเกินไปจนต้องเลื่อนขึ้นลงทุก 5 บรรทัด
      เรื่อง type/class ก็เหมือนกัน จึงไม่จำเป็นต้องเอา enum 4 ค่าที่ใช้เฉพาะใน DTO นี้ไปไว้อีกไฟล์หนึ่ง
  • เป็นบทความที่น่าสนใจ แต่ยังไม่ค่อยน่าพอใจนัก
    มันรีบกระโดดไปหาข้อสรุปเร็วเกินไป แล้วสุดท้ายก็ย้อนกลับไปที่ รสนิยมส่วนบุคคล อีกครั้ง ฉันเห็นด้วยกับรสนิยมหลายแบบ แต่ตัวบทความเองตั้งใจเขียนเพื่อพยายามก้าวข้ามเรื่องรสนิยมอย่างชัดเจน
    คำพูดที่ว่า “ควรหลีกเลี่ยงโอเปอเรเตอร์หรือ syntax sugar เฉพาะภาษา เพราะเป็นภาระต่อผู้อ่าน” ไม่ได้ตามมาจากตัวชี้วัดนั้น ถ้าในฟังก์ชันมีโอเปอเรเตอร์ต่างกัน 3 ตัว และโอเปอเรเตอร์เฉพาะภาษาตัวเดียวแทนทั้งสามได้ในครั้งเดียว “ความพยายาม” ของฟังก์ชันก็ลดลง
    องค์ประกอบอย่าง map/reduce/filter ถ้าใช้ดี ๆ ก็สามารถแทนโอเปอเรเตอร์อื่นและลด “ปริมาตร” ได้ จึงไปได้ทั้งสองทาง
    ตัวอย่าง ?. ดูเป็นข้อวินิจฉัยที่เฉพาะกับภาษามาก โดยชี้ไปที่การออกแบบภาษาที่อ่านยากของ JavaScript ในหลายภาษานั้น null กับ undefined ไม่ได้แยกจากกัน จึงมักเรียกมันว่า null-safe operator
    ข้อความว่า “variable shadowing แย่มาก” กับ “อายุการมีชีวิตที่ยาวขึ้นทำให้ต้องเก็บตัวแปรไว้ในหัวมากขึ้น” อาจขัดกันเอง
    ในบางบริบทฉันชอบ variable shadowing มาก เพราะมันช่วยเอาอินสแตนซ์ก่อนหน้าออกจากสโคป แทนที่จะปล่อยให้ยังเข้าถึงได้ตลอด

  • มีปลั๊กอินเจ๋ง ๆ สำหรับ VS Code ชื่อ Highlight
    มันทำให้ใส่สีต่างกันในโค้ดด้วย regex แบบกำหนดเองได้ และการใช้งานที่พบบ่อยคงเป็นทำ //TODO ให้เป็นสีเหลือง
    ฉันใช้มันทำให้ log ดูจางลง เพราะพอแทรก log ไว้ทั่ว ๆ แล้วมันกลายเป็น noise ทางสายตาเยอะมาก
    ไลบรารีที่ฉันดูแลใช้ log แบบ this.logger?.info('Some logs here'); และฉันตั้ง opacity 0.4 ให้มันถอยไปอยู่ด้านหลัง
    มันยังมองเห็นได้อยู่ แต่สิ่งที่เด่นขึ้นมาในแวบแรกจะเป็น business logic จริง ๆ
    สามารถตั้งค่าแบบนี้ได้: "highlight.regexes": { "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": { "regexFlags": "gmi", "decorations": [{ "opacity": "0.4" }] } }
    https://marketplace.visualstudio.com/items?itemName=fabiospa...

  • ฉันไม่เห็นด้วยกับคำพูดที่ว่าการแบ่งฟังก์ชันเชนยาว ๆ หรือ callback ออกเป็นกลุ่มเล็ก ๆ แล้วใช้ตัวแปรที่ตั้งชื่อดีนั้น มีประสิทธิภาพน้อยกว่านิดหน่อย
    สองเวอร์ชันอาจมีประสิทธิภาพเท่ากันเป๊ะก็ได้
    ทั้งสองกรณีมีการจัดสรรอ็อบเจ็กต์เหมือนกัน เก็บลง heap เหมือนกัน และถูก garbage collection เหมือนกัน ความต่างด้านประสิทธิภาพขึ้นอยู่กับคอมไพเลอร์
    ในเวอร์ชันที่สอง คอมไพเลอร์ควรมองออกว่าตัวแปรแต่ละตัวถูกใช้แค่ทันทีหลังประกาศ และจึงควรจัดการอ็อบเจ็กต์เหล่านั้นให้พ้นสโคปได้เหมือนกับการเรียกแบบเชน

    • เห็นด้วย หลังคอมไพล์แล้วคอมไพเลอร์ก็น่าจะไม่สนว่าคุณตั้งชื่อให้ค่าที่คืนมากลับมาหรือไม่
      แน่นอนว่านี่ตั้งอยู่บนสมมติฐานว่าคุณปล่อยให้มันอนุมานชนิดของตัวแปร
      ต้นทุนที่เห็นจริงมักเกิดตอนคุณ materialize ค่ากลางอย่างชัดเจนเพื่อจะดูมันในดีบักเกอร์ เช่น แปลงเป็นลิสต์ ซึ่งทำให้เกิดการจัดสรรที่เลี่ยงได้และมีต้นทุนตามมา
  • ความพยายามจะทำให้ “ความอ่านง่าย” วัดเชิงปริมาณได้เป็นเรื่องดี เราต้องการแนวทางแบบนี้มากกว่านี้มาก
    ตอนนี้นิยามความอ่านง่ายที่พบบ่อยที่สุดให้ความรู้สึกใกล้เคียงกับ “สิ่งที่ฉันอ่านง่าย”
    ถ้าเอาโค้ดไปให้คนจำนวนมากดู แล้วจับเวลาขณะให้พวกเขาเลือกประโยคที่อธิบายว่าโค้ดนั้นทำอะไร เราอาจค้นพบมิติที่แท้จริงของความอ่านง่ายได้
    โจทย์ที่มีคนตอบถูกมากที่สุดด้วยเวลาเฉลี่ยสั้นที่สุด ก็จะกลายเป็นตัวอย่างของ โค้ดที่อ่านง่ายในโลกจริง และที่สำคัญกว่านั้นคืออาจช่วยระบุแนวปฏิบัติที่อ่านยากจริง ๆ ได้
    ผู้ตอบน่าจะถูกจัดกลุ่มตามแกนอย่าง “ประสบการณ์เขียนโปรแกรม” หรือ “เข้าใจพาราไดม์ X หรือไม่” และผลลัพธ์ก็อาจเลื่อนไปตามกาลเวลาเมื่อกระแสนิยมเปลี่ยน

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