4 คะแนน โดย GN⁺ 2025-08-20 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • แนวทางการเขียนโปรแกรมจากซ้ายไปขวา ทำให้โปรแกรมยังคงอยู่ในสถานะที่ถูกต้องทันทีที่พิมพ์โค้ดลงไป ส่งผลให้ การรองรับจากเครื่องมือ อย่างการเติมโค้ดอัตโนมัติของเอดิเตอร์ทำงานได้เต็มประสิทธิภาพ
  • list comprehension ของ Python รบกวนการทำงานของการเติมโค้ดอัตโนมัติ เนื่องจากมีตัวแปรที่ยังไม่ได้ประกาศและไม่มีการอนุมานชนิดข้อมูล
  • Rust และ JavaScript สามารถประกอบโปรแกรมจากซ้ายไปขวาได้อย่างเป็นธรรมชาติ ทำให้การใช้งานตัวแปรและการสำรวจเมธอดทำได้ตรงไปตรงมามากกว่า
  • สไตล์แบบฟังก์ชันใน C และ Python ทำให้ชื่อฟังก์ชันหรือโครงสร้างค้นพบได้ยาก จนลดทอนประสบการณ์การเขียนโค้ดอย่างมีประสิทธิภาพ
  • สำหรับ ลอจิกที่มีความซับซ้อนสูง โค้ดที่คลี่ออกจากซ้ายไปขวาจะอ่านง่ายกว่า และมีข้อได้เปรียบด้านการบำรุงรักษาและการขยายต่อ

การเขียนโปรแกรมจากซ้ายไปขวา

โค้ดควรถูกต้องทันทีที่พิมพ์ลงไป


ข้อจำกัดของ Python list comprehension

  • ไวยากรณ์ list comprehension ของ Python อย่าง words_on_lines = [line.split() for line in text.splitlines()] มีปัญหาเพราะต้องเข้าถึงตัวแปร (line) ที่ยังไม่ได้ประกาศ ทำให้เอดิเตอร์ไม่สามารถให้ การเติมโค้ดอัตโนมัติ หรือการอนุมานชนิดข้อมูลได้อย่างเหมาะสม
  • ระหว่างกระบวนการพิมพ์โค้ดทีละส่วน
    • หากพิมพ์เป็น words_on_lines = [line.sp เอดิเตอร์จะไม่รู้ชนิดของ line จึงไม่สามารถแนะนำเมธอดได้
    • ยังตรวจจับข้อผิดพลาดที่อาจเกิดขึ้น เช่น การพิมพ์ชื่อตัวแปรผิด (lime เป็นต้น) ได้ยาก
  • หากต้องการรับคำแนะนำที่ถูกต้อง ผู้ใช้กลับต้องเขียนโค้ดที่ยังไม่เสร็จให้ครบในระดับหนึ่ง ซึ่งเป็นกระบวนการที่ ไม่เป็นธรรมชาติและไม่สะดวก

การประกอบจากซ้ายไปขวาใน Rust

  • ตัวอย่างใน Rust (let words_on_lines = text.lines().map(|line| line.split_whitespace());) แสดงให้เห็นว่า
    • เมื่อมีการประกาศฟังก์ชันนิรนาม ตัวแปร (line) ที่ปรากฏขึ้นครั้งแรกจะถือว่าเป็นการประกาศทันที จึงสามารถใช้ การเติมโค้ดอัตโนมัติและคำแนะนำเมธอด ได้ในทันที
    • เมธอด split_whitespace เองก็ถูกค้นพบได้ง่ายเพราะมีระบบแนะนำอัตโนมัติ
  • วิธีนี้ทำให้โปรแกรมยังคงอยู่ใน สถานะที่ถูกต้องอย่างน้อยบางส่วนเสมอ ดังนั้น IDE หรือเอดิเตอร์จึงช่วยรองรับการเขียนโค้ดแบบเรียลไทม์ได้

การเปิดเผยแบบค่อยเป็นค่อยไป (Progressive Disclosure) และการใช้งาน API

  • การเปิดเผยแบบค่อยเป็นค่อยไป (Progressive Disclosure) คือหลักการออกแบบที่ให้ผู้ใช้พบความซับซ้อนเท่าที่จำเป็น และสามารถนำมาใช้กับการเขียนโปรแกรมได้
    • ตัวอย่าง: คล้ายกับ UX ของโปรแกรมประมวลผลคำที่จะแสดงตัวเลือกที่เกี่ยวข้องเฉพาะเมื่อเพิ่มรูปภาพ
  • ภาษา C ขาดการรองรับลักษณะนี้
    • ฟังก์ชันทั้งหมดที่เกี่ยวข้องกับ FILE *file ไม่ได้ถูกสำรวจผ่าน file. จึงต้องจำรูปแบบชื่อฟังก์ชันอย่าง fread, fclose เป็นต้น และยากต่อการค้นพบความสามารถต่าง ๆ
    • ในทางกลับกัน หากเป็นภาษาที่ออกแบบได้เหมาะสม ผู้ใช้ควรค้นพบความสามารถที่เกี่ยวข้องได้แบบ ค่อยเป็นค่อยไป ผ่านคำแนะนำเมธอดจาก file.

ความแตกต่างด้านการค้นพบได้ของฟังก์ชันและเมธอด

  • เปรียบเทียบตัวอย่าง map(len, text.split()) ของ Python กับ text.split(" ").map(word => word.length) ของ JavaScript
    • ใน Python ชื่อฟังก์ชันอย่าง len, length, size คาดเดาได้ยาก จึงอาจต้องลองหลายครั้งกว่าจะรู้ว่าคำสั่งใดใช้ได้จริง
    • ใน JavaScript เพียงพิมพ์ word. แล้วตามด้วย .l เอดิเตอร์ก็สามารถเสนอ length และเมธอดอื่น ๆ ได้ จึงมี ความสามารถในการค้นพบสูงกว่า
    • แม้แต่ฟังก์ชันระดับสูงอย่าง map ก็ยังทำให้ค่าที่ส่งกลับและชนิดข้อมูลที่เกี่ยวข้องปรากฏชัดได้ทันที

ยิ่งลอจิกซับซ้อน ยิ่งเห็นข้อดีของการเขียนแบบมีโครงสร้าง

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

หลักการสำคัญ

โค้ดควรถูกต้องในทุกขณะที่พิมพ์

  • แม้พิมพ์เพียง text โปรแกรมก็ยังคงอยู่ในสถานะที่ถูกต้อง
  • เมื่อเขียนถึง text.split(" ") แล้ว และพิมพ์ต่อเป็น .map(word => word.length) สถานะระหว่างทางทั้งหมดก็ยัง คงความถูกต้องอยู่เสมอ
  • รูปแบบการเขียนโค้ดเช่นนี้ช่วยเพิ่มโอกาสให้เอดิเตอร์ รองรับแบบเรียลไทม์ ได้ดีขึ้น และในสภาพแวดล้อม REPL ก็สามารถเห็นผลลัพธ์ได้ทันที

บทสรุป

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

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

 
GN⁺ 2025-08-20
ความคิดเห็นใน Hacker News
  • ข้อเสียอย่างหนึ่งของ SQL คือคำสั่ง query เริ่มที่ SELECT ไม่ใช่ FROM ทำให้มองไม่ออกทันทีว่ากำลังจัดการกับเอนทิตี (ตาราง) ไหนอยู่ และยังขัดขวางการที่ smart editor จะช่วยเขียน query ได้อย่างมีประสิทธิภาพมากขึ้นด้วย การเรียงเป็น FROM -> SELECT -> WHERE ดูเป็นธรรมชาติกว่า โดยเฉพาะเพราะเราตั้งชื่อคอลัมน์ใน SELECT clause แล้วค่อยอ้างอิงมันใน WHERE จริง ๆ แล้วคิดว่าถ้าใช้แค่ FROM table แทน SELECT * FROM table ก็น่าจะละ SELECT clause ไปได้ด้วยซ้ำ ถึงจะฟังเหมือนคนแก่ช่างบ่น แต่ก็เป็นแค่ความค้างคาใจส่วนตัวของผม
    • PSQL และ PRQL ใช้ลำดับ query ที่ให้ FROM มาก่อนจริง ๆ และใน BigQuery ก็เพิ่งเพิ่มไวยากรณ์แบบ pipe/arrow เข้ามาไม่นานนี้ อีกทั้งยังมี community extension ของ DuckDB ด้วย แนะนำเลย DuckDB - PSQL, DuckDB - PRQL
    • เหตุผลที่ SQL เป็นแบบนี้ก็เพราะพื้นฐาน relational algebra จะเขียน projection ก่อนเสมอ ดังนั้นตามมาตรฐานแล้วจึงใช้ column alias ใน WHERE ไม่ได้ เพราะ selection (WHERE) เกิดก่อน projection (SELECT) และเผื่อไว้เป็นข้อมูลเพิ่มเติม MySQL 8 ก็มีไวยากรณ์ TABLE <table> ด้วย
    • ภายในจริง ๆ แล้ว SQL engine ส่วนใหญ่ประมวลผลตามลำดับ FROM -> WHERE -> SELECT ดังนั้น column alias ที่นิยามใน SELECT จึงใช้ได้ใน GROUP BY, HAVING, ORDER BY แต่ใช้ใน WHERE ไม่ได้
    • ใน C# ก็มี DSL (LINQ-to-SQL) ที่คอมไพล์เป็น SQL ซึ่งใช้โครงสร้างที่ FROM มาก่อนเช่นกัน และผมคิดว่าโครงสร้างแบบนี้ดีเพราะเวลาเขียน clause อื่น ๆ ใน IDE จะได้คำแนะนำฟิลด์จากระบบ autocomplete ทันที
    • Kusto ซึ่งเป็นภาษา query สำหรับการวิเคราะห์ข้อมูลของ Azure ก็มีรูปแบบคล้ายกันโดยใช้ pipe แนะนำ Kusto query และสไตล์ LINQ ของ .NET ก็เช่นกัน พูดตามตรง ผมคิดว่า SQL ควรนำรูปแบบที่เริ่มด้วย FROM เข้ามาใช้อย่างจริงจังกว่านี้ และมันก็ไม่ใช่เรื่องยากอะไรด้วย ผมมองว่ายังขาดความพยายามเพื่อปรับปรุงการใช้งาน
  • ผมคิดว่ายากจะเข้าใจว่าทำไม Python ถึงได้รับความรักมากขนาดนี้ พอมีคนทำงานเกินสองคน ภาษานี้ก็ชวนทรมานได้ไม่สิ้นสุด สิ่งที่ผู้เขียนชี้ให้เห็นเป็นแค่ส่วนยอดของภูเขาน้ำแข็งเท่านั้น
    • ผมคิดว่าคล้ายกับเหตุผลที่คนไม่ได้แห่กันไปใช้ภาษาตระกูล Lisp ความเคร่งครัดทางคณิตศาสตร์ไม่ได้แปลว่าอ่านง่ายเสมอไป list/dict/set comprehension ของ Python ก็คล้ายกับ for loop ที่กำหนดชนิดไว้แล้ว ทุกคนกังวลว่า type ของ Python หลวมเกินไป แต่ไวยากรณ์เดียวที่กำหนด return type ได้ชัดเจนอย่าง list comprehension กลับกลายเป็นเป้าโจมตีก็ดูแปลกดี ในภาษาอื่นส่วนใหญ่รวมถึง Rust ก็ไม่ได้ใช้ลำดับแบบ "from iter as var" และถ้าลองเทียบไวยากรณ์การเรียกฟังก์ชันของแต่ละภาษาก็น่าสนใจเหมือนกัน (ใน Python เองก็มี functools.map เช่นกัน)
    • แค่เพราะคุณไม่เข้าใจอะไร มันไม่ได้กลายเป็นคุณธรรมขึ้นมา ผมเชื่อว่าต้องมีเหตุผลบางอย่างแน่ ๆ ที่ Python ได้รับความนิยม แน่นอนว่ามันมีข้อเสียชัดเจน แต่แค่นั้นยังไม่มีความหมายอะไร ต้องชั่งน้ำหนักข้อดีข้อเสียโดยรวม และเทียบกับภาษาอื่นแบบเดียวกันด้วย
    • ผมก็ชอบ Python เหมือนกัน (แต่ในกรณีทีมเล็ก โปรแกรมสั้น และอายุการใช้งานสั้น) มันไม่มี static typing เลยทำให้พัฒนาได้เร็ว แต่ก็ไม่ได้พังยับเยินเพราะมีระบบ type ที่แข็งแรงพอสมควร ผมคิดว่านี่เป็นเหตุผลที่มันนิยมในสาย data science เพราะมันสะดวกมากเวลาทดลองสำรวจ แต่ถ้าต้องให้หลายทีมดูแลระยะยาวหรือเป็นโปรแกรมขนาดใหญ่ ก็มีข้อเสียชัดเจน สุดท้ายแล้วไม่มีภาษาที่ใช้ได้สารพัดอย่าง อย่างน้อยก็ควรมีทั้งภาษาแบบ "ลองเร็วไปเร็ว (Soft)" และภาษาแบบ "ดูแลระยะยาวได้ดี (Hard)"
    • เมื่อก่อนผมเห็นด้วยกับความเห็นนี้เต็มที่ แต่พอมี type annotation และ type checking แล้ว การทำงานร่วมกับโค้ด Python ที่คนอื่นเขียนก็ง่ายขึ้นมาก ผมยังไม่คิดว่ามันเหมาะถึงระดับโปรเจกต์ใหญ่ แต่พอมี type แล้ว Python ก็กลายเป็นภาษา scripting ที่ผมชอบที่สุด
    • ใน codebase ที่ใช้ร่วมกัน ผมเองก็หลีกเลี่ยง list comprehension และมุ่งไปที่สไตล์ Python ที่เรียบง่ายมาก ๆ มันเป็นภาษาที่ขึ้นชื่อว่า "ควรมีวิธีเดียว" แต่ในความจริงกลับมีหลายวิธีเกินไป list comprehension สนุกและน่าพอใจสำหรับผมเป็นการส่วนตัว แต่ถ้าทุกคนต้องเดินเส้นทางเดียวกันจริง ๆ ไวยากรณ์นี้ก็ควรไม่มี
  • ผมเห็นด้วยกับคำกล่าวที่ว่า "โปรแกรมควรต้องใช้ได้ทันทีตั้งแต่กำลังพิมพ์" แต่ในโลกจริงเราไม่ได้เขียนโค้ดแบบซ้ายไปขวา ทีละบรรทัด ตามลำดับเสมอไป บ่อยครั้งก็เขียนส่วนอื่นก่อน หรือประกาศตัวแปรทีหลัง เช่น ใช้ตัวแปรไปก่อนแล้วค่อยไปประกาศอีกตั้งนานหลังจากนั้น
    • เมื่อคิดว่าโค้ดถูกเขียนครั้งเดียวแต่ถูกอ่านเป็นสิบเป็นร้อยครั้ง ผมคิดว่าโค้ดที่อ่านตามลำดับได้ย่อมอ่านง่ายกว่าโค้ดที่ต้องกระโดดไปมาอย่างมาก
    • จริง ๆ แล้วประเด็นนี้ออกนอกแกนหลักของบทความไปนิดหน่อย แต่ก็เป็นมุมมองที่น่าสนใจ
    • เห็นด้วยอย่างยิ่ง ผมเขียนโค้ดเรียงลำดับตั้งแต่ต้นจริง ๆ ก็เฉพาะตอนสร้างไฟล์ใหม่เท่านั้น เวลาเพิ่มฟิลด์ ผมไม่จำเป็นต้องไปเริ่มที่ class definition แต่จะไปเขียนโค้ดที่ใช้ฟิลด์นั้นก่อนเลย เวลาแก้เงื่อนไขก็เหมือนกัน ชั่วคราวมันมักจะอยู่ในสถานะที่ยังใช้ไม่ได้ (มี error)
    • ผมก็เห็นด้วยกับความเห็นนี้ แต่หลักการสำคัญที่เชื่อมกับมันคือ โครงสร้างแบบที่ "คุณยังเขียนไม่เสร็จ เลยไม่ให้คอมไพล์เลย" นั้นเกินไปหน่อย error ควรเป็นแบบไม่ขัดขวาง แต่บางภาษากลับกันโค้ดที่ยังไม่เสร็จก็ถูกบล็อกไปเลย (เช่น ตัวแปรที่ไม่ได้ใช้, return ที่ขาดหาย ฯลฯ)
    • บางครั้งผมก็รู้สึกไม่สะดวกนิดหน่อยเพราะ IDE ดูเหมือนไม่เข้าใจลำดับที่ผมเขียนโค้ดจริง ๆ
  • IDE บางตัวมีฟังก์ชัน code template ที่พอพิมพ์คำย่อแล้วจะขยายเป็นโครงสร้างโค้ด และให้กดแท็บเพื่อกรอก placeholder แต่ละตำแหน่ง ซึ่งลำดับการกดแท็บไม่จำเป็นต้องเป็นซ้ายไปขวาเสมอไป เช่น อาจกำหนดเป็น {3} for {2} in {1} ก็ได้ เครื่องมือแบบนี้เป็นจุดประนีประนอมระหว่าง "ไวยากรณ์ที่อ่านง่าย" กับ "ไวยากรณ์ที่พิมพ์ง่าย" สำหรับผม ผมเอนเอียงไปทางให้ความสำคัญกับไวยากรณ์ที่อ่านง่าย แม้ต้องพึ่ง tooling ก็ตาม ผมไม่ได้คิดว่าจำเป็นต้องยึดติดกับโครงสร้าง "for-in" อย่างเดียว
  • ช่วงนี้บรรยากาศฉันทามติบน Hacker News คือ Python พลาดที่ไม่มี pipe operator ผมเองตอนย้ายจาก Mathematica ไป R ก็เข้าใจคุณค่าของ pipe ได้เร็วมาก เวลาเขียนโค้ดแปลงข้อมูลทีละขั้นใน data science มันทั้งตรงไปตรงมาและอ่านง่ายมาก Python ถูกใช้ในหลายบริบท แต่ผมก็สงสัยว่าในบริบทอื่นนอกเหนือจากการวิเคราะห์ข้อมูล pipe จะมีข้อดีไหม และพยายามทำความเข้าใจว่าทำไม Python ถึงไม่รับมันมาใช้
    • ถ้าขยับไปไกลกว่า pipe operator อีกนิด ผมคิดว่า reverse assignment ก็น่าลองเหมือนกัน แทนที่จะเขียนกำหนดผลลัพธ์ให้ตัวแปรแบบ 'let foo = ...' ก็อยากลองรูปแบบอย่าง '... =: foo' ดู
    • pipe operator ของ R (โดยเฉพาะ tidyverse R) คือ "killer app" ที่สำคัญที่สุดสำหรับผม ผมไม่คิดว่าจะมีภาษาอื่นที่ทำงานกับข้อมูลได้ง่ายและสนุกเท่านี้อีกแล้ว เช่น แทนที่จะซ้อนแบบ bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12) ถ้าใช้ pipe เป็น mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) จะง่ายและดูดีกว่ามาก
    • ถ้าใช้ไวยากรณ์ pipe ใน pandas ของ Python จะได้แบบนี้
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      ส่วนใน R คือ
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      ทั้งคู่ก็อ่านได้ดี แต่ข้อดีของ R คือ pipe ทำงานได้นอก data frame ได้ดีกว่าด้วย
  • การถกเถียงนี้แทบจะเป็นสงครามศาสนา พอ ๆ กับการถกเถียงภาษา FP (functional) vs OOP (object-oriented) หรือ vim กับ emacs โดยใน vim จะเป็นแบบตัวดำเนินการมาก่อน ส่วน emacs จะเป็นแบบการเลือกมาก่อน ภาษาที่ "อ่านแบบภาษาอังกฤษ" มักมีโครงสร้างกริยานำหน้า (เช่น Lisp/Scheme) ขณะที่ภาษาที่วางกริยาไว้ท้ายประโยค เช่น เยอรมันหรือทมิฬ จะเข้ากับสไตล์ OOP (คำนามมาก่อน) มากกว่า เช่น ในภาษาทมิฬจะเป็น "water drink" ส่วนภาษาอังกฤษคือ "drink water" เพราะแบบนี้บางคนจึงอาจรู้สึกว่า vim ใช้ง่ายกว่า ไม่ได้หมายความว่าสไตล์ไหนดีกว่า แต่แต่ละแบบก็ถูกสร้างให้เหมาะกับเครื่องมือ/นิสัยของคนต่างกัน และทุกวันนี้ถ้ามี language model ก็ดูเหมือนจะทำอะไรได้เกือบหมดแล้ว
    • เรื่อง "ถ้าออกแบบให้เหมือนภาษาอังกฤษต้องเป็นกริยานำหน้าหรือไม่" ถ้าเป็นภาษาเชิงคำสั่งก็คงใช่ แต่ถ้าเป็นภาษาเชิงประกาศแล้วให้เหมือนภาษาอังกฤษ ก็ต้องเป็นประธานขึ้นก่อน
    • สำหรับคำถามว่า "ภาษาเยอรมันเอากริยาไว้ท้ายเสมอหรือ" จริง ๆ แล้วประโยคง่าย ๆ จะเอากริยาไว้ลำดับที่สอง ("I drink water" → "Ich trinke Wasser") ไม่ได้อยู่ท้ายประโยคเสมอไป
    • ส่วนเรื่องที่บอกว่า vim เป็นแบบตัวดำเนินการมาก่อน จริง ๆ แล้ว Kakoune ทำตรงกันข้าม และผมคิดว่าวิธีนี้มีเหตุผลกว่ามาก คำอธิบายเกี่ยวกับ Kakoune
  • ในอีกมุมหนึ่ง ไวยากรณ์ from some_library import child_module ของ Python นั้นตรงไปตรงมามาก ส่วนใน JS จะเป็นโครงสร้างอย่าง import { asYetUnknownModule } from SomeLibrary ซึ่งให้ความรู้สึกไม่ตรงไปตรงมาน้อยกว่าเยอะ
    • ใน JS ถ้าใช้ namespace import แบบนี้
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      จริง ๆ แล้ว IDE autocomplete ทำงานได้ดีมาก เลยคิดว่าเป็นทางเลือกที่ดี คำอธิบาย MDN เรื่อง namespace import
    • ผมสงสัยว่าทำไมถึงยึดติดกับคีย์เวิร์ด "from" กันนัก แค่เขียนเป็น
      import SomeLibrary {
        asYetUnknownModule
      }
      
      แบบนี้ก็น่าจะพอ
  • ReScript เปลี่ยน API จาก data-last เป็น data-first ก็เพราะเหตุผลนี้ ด้วย type inference ที่ยอดเยี่ยม มันให้ autocomplete ที่ถูกต้องและตรง type แทบตลอด ทำให้ประสบการณ์พัฒนาดีมาก แน่นอนว่าถ้าประกาศฟังก์ชันโดยไม่มีการอ้างอิงเลย (จึงไม่รู้ type) ก็ยังมีปัญหาอยู่ แต่แก้ได้ด้วยการใส่ type หรือเรียกใช้มันก่อน แนะนำบทความนี้ด้วย เปรียบเทียบ Data-first และ data-last
  • ผมพูดมุมมองแบบนี้มาตลอด และมันก็เชื่อมกับเหตุผลที่ Ruby ให้ความรู้สึกง่ายกว่าสำหรับผมมากด้วย โดยเฉพาะเพราะผมเองก็ไม่ได้เคยใช้ทั้ง Python และ Ruby ในระดับ production อย่างลึกซึ้ง แต่ก็ยังไม่ค่อยเข้าใจว่าทำไม Python ถึงถูกติดตั้งแพร่หลายและเริ่มถูกใช้กว้างขวางขนาดนั้น Ruby เองก็ไม่ได้ไม่มีข้อเสีย แต่ดูเหมือนคนส่วนใหญ่ที่เขียนสคริปต์ไม่ได้ต้องเจอกับการเปลี่ยนแปลงซับซ้อนแบบที่ Python มี อย่างน้อย Ruby ก็ไม่มีเหตุการณ์ชนกันของเวอร์ชันใหญ่แบบหนัก ๆ ในช่วง 10 ปีที่ผ่านมา
  • โดยรวมแล้วผมเห็นด้วยเต็มที่กับประเด็นที่บทความชี้ไว้ ผมคิดว่าโครงสร้างที่วางบริบทไว้ก่อนและอ่านจากซ้ายไปขวาจะเข้ากับ LLM หรือ autocomplete ได้ดีกว่า แต่ตัวอย่างโค้ดนั้น แทนที่จะเขียนแบบ len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))) ผมคิดว่าใช้ NumPy array จะดีกว่า เพราะไม่ต้องสร้างลิสต์ใหม่ในหน่วยความจำและยังจัดการทั้งบรรทัดได้พร้อมกันดีกว่าด้วย เช่น
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    แบบนี้สะท้อนความเป็น "ซ้ายไปขวา" ได้มากกว่าเยอะ
    • เวอร์ชัน numpy เองก็ยังดูคล้ายรหัสลับอยู่บ้าง ("line > 0" ยังพอไหว แต่กฎ broadcasting อาจซับซ้อนได้) ตัวอย่าง JavaScript ของผู้เขียนหรือ collection API ของภาษาที่เข้มงวดเรื่อง type อย่าง C#, Java, Scala ดูสะอาดกว่ามาก ส่วนตัวผมชอบ Kotlin เพราะเขียนได้แบบนี้
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }