2 คะแนน โดย GN⁺ 2025-01-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • วัฒนธรรมการพัฒนาที่การติดตั้งแพ็กเกจกลายเป็นค่าเริ่มต้น ทำให้เกิด dependency churn ที่นำไปสู่การอัปเดต แพตช์ การตรวจสอบ และการจัดการทรานซิทีฟดีเพนเดนซี สร้างต้นทุนแฝงต่อผลิตภาพ
  • ยิ่งเป็นระบบนิเวศที่มีระบบแพ็กเกจสมบูรณ์อย่าง JavaScript และ Rust ผลกระทบก็ยิ่งมาก โดยมีตัวอย่างเช่นโปรเจกต์ Tokio ใหม่ดึงมา 28 crate, Rocket 172 crate และ MiniJinja CLI 142 dependencies
  • แม้ฟังก์ชันที่เสถียรมานานอย่าง terminal_size ก็ยังเกิดความย้อนแย้งที่ต้องแบกรับ crate เพิ่มและการออกรุ่นซ้ำ ๆ เพราะการเปลี่ยนแปลงของ ไลบรารี abstraction ของแพลตฟอร์ม
  • สำหรับฟังก์ชันเล็ก ๆ การสร้าง implementation แบบไร้ dependency ด้วย ChatGPT หรือ Cursor อาจเร็วกว่าไล่หา dependency และคอยอัปเกรดต่อเนื่อง อีกทั้งยังลดภาระการคอมไพล์โค้ดภายนอก
  • ไลบรารีในโดเมนยาก ๆ อย่าง HTTP, QUIC, กราฟิก, tokio ยังจำเป็น แต่การยอมรับกราฟ dependency ขนาดใหญ่เพียงเพราะใช้แค่ฟังก์ชันเดียว ควรถูกตั้งคำถามมากขึ้น

ภาวะ treadmill งานดูแลรักษาที่เกิดจาก dependency ที่เพิ่มขึ้น

  • นักพัฒนามักติดตั้งแพ็กเกจได้ง่ายเพราะเหตุผลด้านผลิตภาพ แต่ผลลัพธ์คือการต้องอยู่กับ dependency churn ที่มีทั้งการอัปเดต แพตช์ การตรวจสอบ และการจัดการทรานซิทีฟดีเพนเดนซีอย่างไม่สิ้นสุด
  • ยิ่งระบบนิเวศมีคำตอบด้านแพ็กเกจครบถ้วน ปัญหานี้ยิ่งเด่นชัด โดย JavaScript และ Rust ได้รับผลกระทบมากเป็นพิเศษ
  • กรณีของ Rust:
    • โปรเจกต์ Tokio ใหม่ดึงมา 28 crate
    • โปรเจกต์ Rocket ใหม่ขยายไปได้ถึง 172 crate
    • ตัว MiniJinja เองสามารถมีอยู่ได้ด้วย dependency เดียว แต่เวอร์ชัน CLI ดึงมา 142 dependencies
  • แก่นของปัญหาคือ เพื่อฟังก์ชันเล็ก ๆ เพียงอย่างเดียว เรากลับต้อง build และดูแลโค้ดภายนอกจำนวนมากเกินกว่าที่จำเป็นจริง

ความย้อนแย้งของโค้ดที่เสถียรจาก terminal_size

  • terminal_size คือ crate สำหรับตรวจขนาดเทอร์มินัลตามชื่อ
  • แม้ API พื้นฐานที่ใช้สำหรับฟังก์ชันนี้จะแทบคงที่มาตั้งแต่ยุคแรกของคอมพิวติ้งเทอร์มินัล แต่ก็ยังดึง crate เพิ่ม 3~4 ตัว ตามระบบปฏิบัติการ
  • จึงเกิดสถานการณ์ที่ต้องคอมไพล์ฟังก์ชันอื่นอีกหลายพันตัว เพียงเพื่อเช็กว่าเทอร์มินัลเป็น 80x25 หรือ 120x40
  • crate นี้มีการ ออกรีลีส 26 ครั้ง แต่ implementation ที่เขียนใช้เองในโปรเจกต์เมื่อ 10 ปีก่อนสำหรับฟังก์ชันเดียวกัน ยังทำงานได้อยู่โดยไม่ต้องอัปเดต
  • สาเหตุที่มีรีลีสจำนวนมากไม่ใช่เพราะฟังก์ชันเปลี่ยนไป แต่เพราะ ไลบรารี abstraction ของแพลตฟอร์ม ที่เป็นฐานยังเปลี่ยนอยู่เรื่อย ๆ
  • บน UNIX มีข้อยกเว้นว่าจำเป็นต้องพึ่ง libc
    • เพราะ Rust ไม่เปิดเผยค่าคงที่ libc ของแพลตฟอร์ม และค่าคงที่เหล่านั้นก็ไม่ได้ถูกทำให้เป็นมาตรฐาน
    • อย่างไรก็ดี libc เป็น dependency ที่พบได้ทั่วไปและมีน้ำหนักเบา จึงเลี่ยงได้ยาก

ความปลอดภัยและวัฒนธรรมการนำโค้ดกลับมาใช้ซ้ำยิ่งเสริมแรงให้ dependency

  • แนวทางแบบ “big supply chain” ระวังการคัดลอกฟังก์ชันมาใส่เองหรือการใช้ unsafe โดยตรง และกดดันให้ฝากเรื่องเหล่านี้ไว้กับชั้น abstraction ของแพลตฟอร์ม
  • ยังมีบริษัทที่ขายเครื่องมือสำหรับจัดการปัญหา dependency และใช้ชื่อของความปลอดภัยเพื่อผลักดันให้คง dependency ไว้และอัปเดตให้ใหม่อยู่เสมอ
  • แต่ในอีกด้าน dependency จำนวนมากเองก็อาจเป็น แหล่งสำคัญของปัญหาด้านความปลอดภัย
  • เป้าหมายของโค้ดควรเป็นการเขียนให้ถึงจุดที่เสถียร และไม่จำเป็นต้องอัปเดตอีก
  • ในระบบนิเวศ Rust แม้ dependency จะทำงานได้เสถียร แต่ถ้า bug tracker ค่อนข้างไม่เคลื่อนไหว ก็อาจได้คะแนนต่ำใน RUSTSEC
  • วัฒนธรรม code review แบบองค์กรก็ส่งอิทธิพลต่อโอเพนซอร์สเช่นกัน
    • วิศวกรที่นำไลบรารีใหม่แวววาวเข้ามา มักมีโอกาสได้รับรางวัลมากกว่าถูกตำหนิ
    • ผลลัพธ์คือมีเครื่องมืออย่าง Dependabot เกิดขึ้น และโปรเจกต์ก็ได้รับ PR อัปเดต dependency เข้ามาอย่างต่อเนื่อง
    • ภายในองค์กร แค่การทำ vendoring, การตรวจสอบภายใน, และการอัปเกรดทั้งองค์กร ก็เพียงพอจะทำให้ทีมวิศวกรรมยุ่งอยู่ตลอด

ฟังก์ชันเล็ก ๆ สามารถสร้างเองได้

  • เส้นทางที่ง่ายกว่าคือเขียนโค้ดที่ต้องใช้ขึ้นมาเอง
  • แม้งานเริ่มต้นจะมากกว่า แต่เมื่อเขียนเสร็จแล้ว โค้ดนั้นไม่ต้องใช้ crate ใหม่ และไม่ต้องรอให้ผู้เขียน upstream แก้ edge case
  • หากโค้ดพังในจุดที่ตัวเองใช้งาน ก็แก้เองได้ และโค้ดที่ยังทำงานได้ไม่จำเป็นต้องขึ้นไปอยู่บน treadmill งานดูแลรักษาเสมอไป
  • ณ ปี 2025 ChatGPT หรือ Cursor สามารถช่วยสร้าง implementation แบบไร้ dependency สำหรับฟังก์ชันเล็ก ๆ ที่พบบ่อยได้อย่างรวดเร็ว
  • ฟังก์ชันเล็ก ๆ จำนวนมากมี maintenance overhead ต่ำ และอาจมีภาระน้อยกว่าการอัปเกรด dependency อย่างต่อเนื่อง
  • ถ้าเป็นโค้ดไม่กี่บรรทัด ก็ไม่จำเป็นต้องคอมไพล์โค้ดหลายพันบรรทัดของคนอื่นเพื่อฟังก์ชันเดียว

ให้คุณค่ากับ dependency ที่ต่ำมากขึ้น

  • ไม่ใช่ว่า dependency ทุกตัวจะเป็นสิ่งไม่ดี
    • ไลบรารีกราฟิกที่ abstraction ไดรเวอร์ซับซ้อน
    • implementation ของโปรโตคอลอย่าง HTTP และ QUIC
    • ไลบรารีสำคัญอย่าง tokio ที่เอาออกไม่ได้และก็ไม่ได้ตั้งใจจะเอาออก
  • แต่ถ้าใช้เพียงฟังก์ชันเดียวแล้วต้องคอมไพล์ฟังก์ชันนับร้อย ก็ควรมองเป็นสัญญาณเตือน
  • เราควรยอมรับการเลือกเขียนฟังก์ชันเล็ก ๆ เองเพื่อหลีกเลี่ยง กราฟทรานซิทีฟดีเพนเดนซี ให้มากขึ้น
  • ควรตั้งข้อสงสัยกับกราฟ crate ขนาดใหญ่ให้มากขึ้น และมองโค้ดที่เรียบง่าย เสถียร และไม่ต้องแตะไปอีกหลายปีในแง่บวก
  • sha1-smol เดิมกลายเป็น crate มาตรฐานสำหรับคำนวณแฮช SHA1 ในชื่อ sha1 แต่ภายหลังได้ยกชื่อให้ rust-crypto และถูกกดดันให้เข้ากับระบบนิเวศ crypto ที่ใหญ่กว่า
    • หากใช้ crate sha1 ใหม่ จะมี 10 dependencies ติดมาด้วย
    • เพราะชื่อใน registry มีความสำคัญ และมีข้อกำหนดเรื่องความเข้ากันได้ของ trait จึงหลีกเลี่ยงได้ยาก
  • MiniJinja เน้นย้ำเรื่อง dependency ต่ำใน README
$ cargo tree
minimal v0.1.0 (examples/minimal)
└── minijinja v2.6.0 (minijinja)
    └── serde v1.0.144
  • MiniJinja มี PR สำหรับลบ dependency ตัวสุดท้ายออก
  • ในสถานการณ์ที่เหมาะสม เราควรชื่นชมการ สร้างใช้เอง และให้การยอมรับมากขึ้นแก่ผู้เขียนที่สร้างไลบรารีโอเพนซอร์สด้วย dependency ที่ต่ำหรือไม่มีเลย

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

 
GN⁺ 2025-01-26
ความคิดเห็นจาก Hacker News
  • ตัวภาษา Rust เองนั้นดี แต่ไม่ชอบ ระบบนิเวศของ dependency ใน Rust หลายคนบ่นว่า C++ เพิ่ม dependency ได้ยาก แต่กลับรู้สึกว่านั่นเป็นเหมือนฟีเจอร์ เพราะมันทำให้ต้องคิดว่าจำเป็นจริงหรือไม่
    ใน C++ ผมควบคุม dependency ได้เอง แต่ใน Rust ไม่นานก็ทะลุ 100 ตัวจนยอมแพ้ไป จากมุมมองด้านความปลอดภัย พูดตรง ๆ ว่าผมไม่อาจรู้ได้เลยว่ากำลังแจกจ่ายอะไรอยู่
    อีกอย่าง Rust ไม่มี ความเข้ากันได้ของ ABI และไม่มีวัฒนธรรม shared library ด้วย เลยรู้สึกเหมือนทำลายโมเดลการแจกจ่ายแพ็กเกจของระบบปฏิบัติการ เวลาเลือก Linux distribution เรากำลังเชื่อใจคนที่ build distribution นั้น แต่ Rust ดูใกล้เคียงกับ Python ที่ใครก็อัปโหลดอะไรบางอย่างขึ้น PyPI ได้มากกว่า

    • Debian/Ubuntu ถือว่าปลอดภัยได้ เพราะ maintainer เป็นผู้เซ็น และ maintainer เหล่านั้นผ่านการตรวจสอบแล้ว สุดท้ายคือโครงสร้างที่เชื่อว่า Debian/Ubuntu อนุญาตให้เฉพาะคนที่น่าเชื่อถือเท่านั้นเซ็นแพ็กเกจได้
      แล้ว Docker/Python/Rust ล่ะ? ผมไม่รู้จักคนที่สร้าง Docker image, แพ็กเกจ PyPI, หรือ Rust crate ของผมเลย
      โดยเนื้อแท้แล้วเรากลับไปสู่ยุคที่ส่ง EXE กับ DLL กันเป็น ZIP อีกครั้ง เพียงแต่ตอนนี้เราเรียกมันว่า container และรันด้วยสิทธิ์ root อย่างภาคภูมิใจ
      อย่างที่ผู้เขียนบอก บางครั้งการรวมซอร์สโค้ดของ dependency เข้าไปตรง ๆ อาจเป็นทางที่ดีที่สุด เมื่อก่อนเรียกสิ่งนี้ว่า vendoring และเคยสำคัญพอสมควรใน Rails/Ruby ข้อดีใหญ่คือภายหลังจะไม่ถูกกระทบจากการยึดแพ็กเกจโดยผู้ไม่หวังดี และถ้าต้องการก็สามารถ merge security patch จาก upstream เองได้
    • ไม่รู้ว่าคนที่มีมุมมองแบบนี้ทำงานอะไรกัน ถึงสามารถสร้างล้อขึ้นมาใหม่ในทุกโปรเจกต์ได้ เพียงเพราะกลัวความเสี่ยงสมมติ ส่วนใหญ่ของความเสี่ยงนั้นยังคงอยู่ในการทำ implementation ภายในบริษัทเองเหมือนเดิม และบั๊กก็เช่นกัน
      นึกไม่ออกเลยว่าโปรเจกต์ไหนที่ กลุ่มอาการ NIH ให้ผลสุทธิเป็นบวก แม้แต่ dependency ที่ไม่จำเป็นอย่างยิ่งก็ช่วยประหยัดเวลาได้มาก
      ถ้าสร้าง dependency แบบเลือกใช้บางส่วนขึ้นใหม่ทุกโปรเจกต์ ก็สงสัยว่าเวลาสำหรับแก้บั๊กเพิ่มเติม edge case, เวอร์ชันหลายชุดของ dependency ชั้นล่าง และการรองรับหลายแพลตฟอร์มจะมาจากไหน
    • ถ้ามี package manager ตั้งแต่แรกและลดแรงเสียดทานในการเพิ่มกับแจกจ่ายแพ็กเกจลงอย่างมาก ระบบนิเวศของภาษาจะสูญเสียสิ่งที่มีประโยชน์มากไป นั่นคือแรงกดดันที่ดีของ การคัดเลือกโดยธรรมชาติ
      ถ้ารวมสิ่งนี้เข้ากับ repository ที่ใครก็อัปโหลดได้และมีการคัดกรองอ่อนอย่าง PyPI, npm, Cargo รวมถึง standard library ขนาดเล็ก ก็ต้องเตรียมใจเจ็บปวดไว้
    • ฟีเจอร์ที่เป็น “ชุดไลบรารีที่ดึงมาใช้ได้ง่าย” กลายเป็น anti-feature เพราะทำให้ดึงไลบรารีสนับสนุนมาใช้ได้ง่ายอย่างนั้นหรือ ดูเหมือนนี่ใกล้เคียงกับปัญหาเรื่อง การเลือกของนักพัฒนา มากกว่า Rust หรือ JS
      นักพัฒนาเป็นคนเลือกว่าจะดึง dependency ตัวไหนมาใช้ dependency ที่ไม่มีใครใช้ก็จะหายไป และเหตุผลที่ไลบรารีส่วนใหญ่มี dependency มาก ก็เพราะนักพัฒนาจำนวนมากชอบสร้างอะไรบางอย่างบน dependency
      ถ้าพูดถึง build system แล้ว Cargo ไม่ได้บังคับให้ใช้ Crates.io แค่ทำให้ง่ายเท่านั้น จะใช้ dependency แบบอิง path เหมือน CMake/Vcpkg, Conan หรือสร้างไลบรารีเองก็ได้
      แม้จะใช้ Crates.io ถ้าไม่ชอบการเปลี่ยนแปลงก็ pin เวอร์ชันไว้ได้ มันแค่ทำให้รับเวอร์ชันล่าสุดได้ง่ายเท่านั้น
      ใน Rust การสร้างซอฟต์แวร์บนซอฟต์แวร์ที่มีอยู่ทำได้ง่าย ถ้าไม่ชอบซอฟต์แวร์เดิมหรือความเร็วในการเปลี่ยนแปลงของมัน ก็อย่าโทษ Cargo ให้ทำตามวิธีที่ต้องการก็พอ
    • ปัญหาจริงของการเพิ่ม dependency ใน C++ คือยากที่จะเพิ่มใน “รูปแบบที่ทุกคน build ได้โดยไม่มีปัญหา” แม้จะใช้ได้บนเครื่องตัวเอง แต่ในสภาพแวดล้อมของผู้ใช้หรือผู้ร่วมพัฒนาที่อาจเกิดขึ้นอาจใช้ไม่ได้ จึงกลายเป็นปัญหา
      distribution ยังคง build แพ็กเกจ Rust จากซอร์ส และ vendor dependency ของ crate ไว้ใน repository อยู่ดี มันเจ็บปวดกว่าเพราะมี dependency มากกว่าและอัปเดตบ่อยกว่า แต่เรื่องนี้แยกจาก shared library
  • คำกล่าวที่ว่า API สำหรับตรวจขนาดเทอร์มินัลเสถียรมานาน 50 ปีนั้นไม่ถูกต้อง TIOCGWINSZ ioctl เท่าที่ผมรู้ไม่เคยถูกทำให้เป็นมาตรฐาน และใน Unix กับ BSD แต่ละตัวก็มีชื่อหลายแบบ
    ฟังก์ชัน tcgetwinsize() เพิ่งเข้า POSIX ในปี 2024 และหัวข้อนี้ทั้งหมดก็มีประวัติที่น่าเสียดายพอสมควร https://news.ycombinator.com/item?id=42039401 หมายความว่าเป็นแบบนั้นตั้งแต่ก่อนจะไปถึงฝั่ง Windows แล้ว

    • ความรู้สึกที่ได้จากบทความคือ implementation ทดแทนนั้นเรียบง่าย ไม่ใช่เพราะมันดีกว่า แต่เพราะ มันจัดการเฉพาะ use case ของตัวเอง มากกว่า
      บางครั้งก็ใช้ได้ แต่สำหรับคนที่มี use case อื่น หรือคนที่รันซอฟต์แวร์บนระบบที่ตนเองไม่เข้าใจและไม่ได้ใช้งาน ซอฟต์แวร์อาจแย่ลงได้
      คุณค่าที่แท้จริงของไลบรารีคือมันช่วยจัดการข้อเท็จจริงที่ว่าแม้ปัญหาที่ดูเรียบง่ายก็มีความซับซ้อนมาก ผมก็ไม่คิดว่า dependency 3–4 ตัวสำหรับไลบรารีที่รันได้ทั้ง Windows/Linux จะมากขนาดนั้น ในกรณีที่ดี คนที่ใช้ไลบรารีนั้นมีประสบการณ์ในขอบเขตนั้นมากกว่า และช่วยจัดการส่วนที่ผมไม่รู้ได้ด้วย
    • แม้จะไม่ถูกทำให้เป็นมาตรฐาน แต่การเรียกเหล่านั้นก็ไม่เปลี่ยนไป โดยเฉพาะ Windows call ที่ถูก compile เข้าไปใน binary จำนวนนับไม่ถ้วน จึงรับประกันความเสถียรของ ABI
      ioctl มีปัญหาแน่นอน แต่การเปลี่ยนแปลงที่ทำให้ terminal-size หรือ dependency ของมันออกหลาย release ไม่เกี่ยวกับค่าคงที่ ioctl/TIOCGWINSZ หรือ struct winsize โค้ดส่วนนั้นไม่ได้เปลี่ยน
    • ในกรณีนี้ crate terminal-size ก็แค่เรียก tcgetwinsize ของ Rustix และ Rustix ก็เรียก tcgetwinsize ของ libc อีกที ดังนั้นถ้าทำสิ่งเดียวกันเองก็ลด dependency ได้ไม่น้อย และราคาที่ต้องจ่ายก็ประมาณ การรองรับ Windows
      API นี้เสถียรมานาน 50 ปีหรือ 25 ปีเป็นรายละเอียดปลีกย่อย dependency ดังกล่าวไม่ได้แม้แต่แสร้งทำเป็นจัดการความซับซ้อนนั้น และโอกาสที่ฟังก์ชันนั้นจะเปลี่ยนหรือถูกเอาออกในอนาคตอันใกล้ก็ต่ำ
    • เทอร์มินัลเป็นตัวอย่างที่ดีของสิ่งที่ดูเรียบง่ายมาก แต่กลายเป็นปัญหาใหญ่เพราะช่วงแรกมีผู้ผลิตมากเกินไปและมาตรฐานอุตสาหกรรมไม่ลงตัว แม้แต่สิ่งที่ใกล้ที่สุดคือ VT100 หรือ VT102 ก็ยังคลุมเครือ
      ส่วนใหญ่เขียนลงไปตรง ๆ ได้ แต่ฟีเจอร์อย่างขนาดเทอร์มินัลหรือ raw mode นั้นเละเทะ และต้องใช้สิ่งอย่าง ioctl พูดตรง ๆ คือไม่ค่อยดี
      แต่ไลบรารียิ่งไม่ดีเข้าไปอีก ถ้าไม่อยาก link กับ ncurses ก็ต้องขอให้พระเจ้าคุ้มครอง
  • เมื่อไม่นานมานี้ ผมได้ชุบชีวิตเว็บแอปสตาร์ทอัพตัวแรกที่สร้างไว้เมื่อปี 2006 ขึ้นมาอีกครั้ง มันเป็นไซต์โซเชียลมีเดียที่เน้นการแชร์สื่อ และตามมาตรฐานในตอนนั้นก็เป็น LAMP stack ที่ค่อนข้างเรียบง่าย ใช้ PHP 5, MySQL 3.2 แต่ก็มีฟีเจอร์โซเชียลมีเดียส่วนใหญ่ครบถ้วนสำหรับยุคนั้น
    เพราะอยากลองใช้เทคโนโลยี CI/CD ใหม่ๆ ด้วยตัวเอง เลยกำลังเอาแอปนี้มา over-engineer เพื่อการเรียนรู้และทำกระบวนการ deploy ขึ้นมา จะใช้ WordPress หรือแอป Hello World ก็ได้ แต่แบบนี้สนุกกว่ามาก
    PHP แทบทั้งหมดเขียนเองทั้งหมด ผมทำไลบรารีสำหรับ authentication/authorization, template, form processing ฯลฯ เองด้วย และใช้ไลบรารี PEAR แค่ตัวเดียวสำหรับส่งอีเมล ส่วน frontend เป็น HTML ล้วนๆ แทบไม่มี JavaScript และใช้ Flash สำหรับเล่นสื่อ ในปี 2006 คนส่วนใหญ่ก็ทำกันแบบนี้
    การรันแอปอายุ 19 ปีนี้อีกครั้งใช้เวลาแค่ประมาณหนึ่งชั่วโมงเท่านั้น เปลี่ยนไดรเวอร์ PHP mysql เก่าเป็น mysqli แล้วปรับ schema กับ query บางส่วนให้เข้ากับ MySQL 8 หลักๆ ก็แค่ครอบคำที่ตอนนี้กลายเป็น reserved word ด้วย backtick และแก้ค่า default ของคอลัมน์ที่เข้มงวดขึ้น สิ่งเดียวที่ใช้ไม่ได้คือ Flash
    ในทางกลับกัน ที่ทำงานปัจจุบันมีแอป Spring Boot ที่เขียนด้วย Java 8 อยู่หลายสิบตัว และมีรายการช่องโหว่จาก dependency หลายสิบตัวกองเป็นหน้ากระดาษ พออัปเดตตัวหนึ่งก็ต้องไล่อัปไลบรารีอื่นๆ ต่อเป็นแถว และกลายเป็นฝันร้ายเพราะ transitive dependency ดังนั้นจึงทำแค่จัดการช่องโหว่ที่ร้ายแรงที่สุดแบบขั้นต่ำ และไม่มีแผนที่เป็นจริงได้ในการอัปเดตทั้งหมด
    ที่ตลกคือสิ่งที่แอป PHP ปี 2006 ทำกับสิ่งที่แอป Spring Boot ปัจจุบันทำนั้นไม่ได้ต่างกันมาก สุดท้ายทั้งหมดก็คือ CRUD เพียงแต่มีเครื่องประดับและเครื่องมือแบบ enterprise แปะอยู่รอบๆ มากกว่ามากเท่านั้น

    • เมื่อมองโลกของ Go และ Linux ตระกูล C ผมก็เริ่มเชื่อใน ปรัชญาไลบรารีแบบหนา คือดึงไลบรารีหนึ่งตัวที่แก้ปัญหาในโดเมนหนึ่งเข้ามา แล้วเติมส่วนที่ต้องการเข้าไปเอง
      ไม่ได้ดึง dependency หนึ่งตัวสำหรับทุกข้อใน checklist อาจมีงานซ้ำบ้าง แต่เส้นทางอัปเกรดง่ายกว่ามาก
    • เทคโนโลยี CI/CD ใหม่ๆ ส่วนใหญ่กลายเป็นมาตรฐานเพราะความซับซ้อนที่เกิดจากการต้องดูแล dependency จาก third party และสภาพแวดล้อมการรันที่เปลี่ยนตลอดเวลา สำหรับแอป LAMP เก่าๆ ที่ deploy ด้วย scp โดยมากแล้วมันไม่ใช่ปัญหา
    • เห็นด้วยบางส่วน แต่ก็ไม่อยากใช้เวลาไปกับการประดิษฐ์ล้อขึ้นมาใหม่แล้วใส่บั๊กลงไปในนั้น
      ตัวอย่างเช่น รูปแบบข้อความสื่อสารมาตรฐานอย่าง FHIR หรือ HL7 คงไม่มีใครอยาก implement นิยามมาตรฐานที่ซับซ้อนทั้งหมดเอง
      ฟังก์ชันเข้ารหัสก็เช่นกัน ถ้าเขียนเองมักจะเป็นการยิงเท้าตัวเอง และประเด็นความปลอดภัยร้ายแรงที่ค้นพบมาตลอดหลายปีก็พิสูจน์เรื่องนี้แล้ว
      ตอนนี้เป็นยุคที่อยากโฟกัสกับการแก้ปัญหาธุรกิจมากกว่าว่า solution ถูกสร้างขึ้นมาอย่างถูกต้องอย่างไร พอ AI เข้ามาแล้ว โค้ดทั้งหมดก็ให้ความรู้สึกเหมือนถูกปิดตาแล้วเอามาต่อๆ กัน ประเด็นนี้จึงยิ่งสำคัญขึ้น
      การใช้เวลาสร้างทุกอย่างเองอาจได้เปรียบในระยะยาว แต่ก่อนอื่นต้องรอดจากการแข่งขันให้ได้ก่อน คู่แข่งอาจยึดตลาดไปแล้วด้วยโค้ดช่วงแรกที่ทำเร็วและทิ้งได้
    • ที่ทำงานมี นโยบาย static analysis ค่อนข้างเข้มงวด และตั้งแต่เดือนเมษายนจะเข้มงวดยิ่งขึ้น อยากรู้ว่าเคยดู https://docs.openrewrite.org/ สำหรับอัปเกรด dependency อัตโนมัติหรือยัง
      ล่าสุดย้ายจาก Java 8, Spring Boot 2, Swagger ไปเป็น Java 17, Spring Boot 3.3, OpenAPI 3 ซึ่งค่อนข้าง painless
      ยังต้องอัป dependency ตรงบางส่วนและ transitive dependency เพิ่มอีก แต่สิ่งกีดขวางใหญ่ที่สุดถูกจัดการไปแล้วด้วย migration
    • จุดสำคัญตรงนี้คือ ใน Rust แทบไม่มีปัญหา transitive dependency จริงๆ ถ้าอัป crate หนึ่งแล้ว public dependency ก็ถูกอัปไปด้วย และ dependency นั้นถูก expose อยู่ใน API จนต้อง interact กัน ก็แน่นอนว่าต้องอัปไปด้วยกัน
      แต่ตราบใดที่ไม่ได้ link กับไลบรารี C private transitive dependency ก็ไม่เกี่ยวเลย ใน dependency tree เดียวกันสามารถมีเวอร์ชันของ crate ที่ไม่ compatible ตาม SemVer ได้มากเท่าที่ต้องการ และถ้าจำเป็นก็พึ่งพาหลายเวอร์ชันโดยตรงได้ด้วย
      ไม่จำเป็นต้องอัปเกรดเป็นชุดใหญ่แบบ Java แค่อัปตัวเดียวที่มีช่องโหว่ก็พอ เท่าที่รู้ C# ก็มีความสามารถคล้ายกัน แต่ verbose กว่าเล็กน้อย
  • เห็นด้วย 100% NodeJS มีผลต่ออาชีพของผมมาก แต่ NPM สร้างบาดแผลทางใจมากกว่าวัยเด็กที่เละเทะเสียอีก
    ลองนึกภาพโปรแกรมเมอร์มือใหม่ที่กำลังทำแอปใหม่เพื่อเอาไปโชว์เพื่อนๆ เขากำลังจะเพิ่ม dependency ใหม่ แต่ไม่เข้ากับ dependency อื่น เลยตัดสินใจอัปเดตทั้งหมด วินาทีถัดมาก็ไม่มีอะไรทำงาน และ Babel ก็กรีดร้อง
    ถึงอย่างไรก็คงคิดว่าเดี๋ยวก็แก้ได้ แต่ไม่นานก็จะเจอ Git issue ที่เปิดค้างไว้ซึ่งบอกว่าแม้แต่สิ่งพื้นฐานก็ใช้ไม่ได้จริงๆ เช่น ใน Expo มี issue เปิดอยู่ว่าโปรเจกต์ React Native ใหม่แบบ default build บน Android ไม่ผ่าน
    ครึ่งหนึ่งเหมือนไม่มีใครสนใจ และในกรณีนั้นวิธีแก้ก็ไม่ได้อยู่ใน ecosystem ของ Node แต่อยู่ที่ไหนสักแห่งใน ecosystem ของ Android ทุกอย่างถูกแปะด้วยเทปผ้าตั้งแต่บนลงล่าง ถึงอย่างนั้น ถ้าโปรเจกต์มูลค่าหลายพันล้านดอลลาร์ยังปล่อย template ที่ใช้งานไม่ได้ออกมาได้ มันก็ทำให้มั่นใจว่าไม่จำเป็นต้องรู้สึกเป็น impostor syndrome เพียงเพราะ side project ของผมครึ่งหนึ่งรันไม่ขึ้น

    • นี่เป็นปัญหาของ Node ไม่ใช่ปัญหาของ Rust dependency ไม่จำเป็นต้อง “ถูกใจกัน” เอง สามารถมีทุกเวอร์ชันพร้อมกันได้ และไม่มีอะไรพัง
    • การที่ Node.js และ npm หลุดจากการควบคุมเมื่อประมาณ 15 ปีก่อนเป็นสัญญาณเตือนสำหรับผม
      ผมเริ่มมองช่วงเวลาที่ดึง dependency ตัวแรกเข้ามาเป็นความล้มเหลวส่วนตัว เพราะตั้งแต่นาทีนั้น แทนที่จะโหลด JS ธรรมดาด้วย script tag ง่ายๆ ก็กลายเป็นว่าต้องมีทั้ง packaging และการจัดระเบียบทั้งหมด เพียงเพื่อใช้ package.json ไฟล์เดียว
  • เป็นสิ่งที่ทำให้ผมแปลกใจเมื่อย้ายจาก Go มาสู่ระบบนิเวศของ Rust โปรเจกต์ Go ที่โตเต็มที่ เช่น เว็บแบ็กเอนด์โปรดักชันของบริษัทหนึ่ง อาจมี dependency แค่ 10–20 ตัว รวมถึง transitive dependency แล้วด้วย
    อย่างที่บทความบอก แม้แต่โปรเจกต์ Rust ขนาดเล็กก็มีแนวโน้มจะมากกว่านั้นมาก และถ้าทำงานแบบ asynchronous ก็แทบจะรับประกันได้เลย
    ผมไม่แน่ใจว่านี่เป็นเพราะวัฒนธรรมหรือเพราะฟีเจอร์ของภาษา เช่น ใน Go interface จะถูก satisfy แบบ implicit จึงไม่จำเป็นต้องดึงอะไรเข้ามาเพื่อบอกว่ากำลัง implement อยู่

    • เหตุผลใหญ่ข้อหนึ่งคือ Go มี standard library ที่ยอดเยี่ยมและสมบูรณ์มาก แต่ Rust ไม่ได้เป็นแบบนั้นจริง ๆ
      สิ่งที่มีอยู่ในภาษา Go หรือ standard library ของ Go แต่ใน Rust ต้องพึ่ง dependency ได้แก่ green thread, channel, regex, HTTP client, HTTP server, time, command-line flag, logger, การอ่าน/เขียน animated GIF เป็นต้น
      ผมไม่ได้ชอบภาษา Go เองมากนัก แต่ในด้านเครื่องมือและ standard library ถือว่านำหน้า และแน่นอนว่าเป็นสิ่งที่ดีที่สุดเท่าที่ผมเคยใช้มา
    • ความต่างระหว่าง Rust กับ Go อยู่ที่ standard library โดยเฉพาะ standard library ของ Rust ตั้งใจออกแบบให้มีขนาดเล็ก
    • standard library ขนาดใหญ่ของ Go ช่วยลดจำนวน dependency ได้อย่างมาก
  • เห็นด้วยกับประเด็นหลักมาก การยอมให้ abstraction ของไลบรารีอื่นถูกเปิดเผยออกไปนอกไลบรารีของตัวเองมี ต้นทุนแฝง มากมาย ไม่ได้หมายความว่าห้ามทำเด็ดขาด แต่ตอนตัดสินใจควรชั่งน้ำหนักต้นทุนในอนาคตให้สมดุล
    ถ้าแพ็กเกจที่ครอบไว้อยู่เปลี่ยนดีไซน์หรือเป้าหมายบ่อย ก็จะเกิดความไม่เสถียร ฟีเจอร์ที่เดิมเป็นรูปแบบการรับประกันที่ง่ายที่สุดในวิศวกรรมซอฟต์แวร์อาจหายไปหรือถูกแยกย่อย
    อีกทั้งยังทำให้เจ้าของที่ไม่ได้เป็นผู้ร่วมพัฒนาโอเพนซอร์สมืออาชีพ แต่เชี่ยวชาญเฉพาะด้าน เข้ามามีส่วนร่วมใน ecosystem ได้ยากขึ้น เมื่อ “ผมสร้างวิธีทำ X ให้เรียบร้อยแล้ว” กลายเป็นการถกเถียง เจรจา และการเมืองนานหลายสัปดาห์ว่า “X ต้องเข้ากับ Y เพราะคนอาจชอบ Z” ก็เป็นการเสียเวลาของทุกคน
    สิ่งหนึ่งที่ผมเรียนรู้มาคือ สิ่งที่เรียบง่ายที่สุดมักอยู่รอดได้นานที่สุด เราควรชม minilith และ monolith กันบ่อยขึ้น นี่ไม่ใช่ปัญหาเฉพาะของ Rust และผมก็เห็นมาแล้วในหลายภาษา ชุมชนโอเพนซอร์สมักผลักดันแพ็กเกจขนาดระดับอะตอมอย่างแรง และผมก็มักสงสัยอยู่บ่อย ๆ ว่านั่นเป็นไปเพื่อปักธงหรือย้ายกรรมสิทธิ์มากกว่าประโยชน์ของชุมชนผู้ใช้จริงหรือไม่

  • ในปี 2025 ผมค่อนข้างบังเอิญมาถึงมุมมองที่ว่า การให้ ChatGPT หรือ Cursor สร้าง implementation แบบไม่มี dependency ของฟังก์ชันทั่วไปหลาย ๆ อย่างนั้นเร็วกกว่า และผมก็เห็นด้วยมากขึ้นเรื่อย ๆ โดยเฉพาะหลังจากเจอ dependency hell ในแอป React ขนาดใหญ่
    ต้องคิดถึง dependency ต่อ SaaS หรือบริการ third-party ด้วย หลายอย่างในนั้นเป็นแพตเทิร์นที่พบบ่อยและเป็นปัญหาที่แก้กันไปแล้ว จึงให้ LLM ทำซ้ำได้อย่างรวดเร็ว

    • เมื่อเทียบกับเมื่อก่อน ตอนนี้ผมมีแนวโน้มมากขึ้นมากที่จะเริ่มจาก ฟังก์ชันยูทิลิตีภายใน ขนาดเล็กที่ AI implement ให้ ก่อนเพิ่มไลบรารี
      สำหรับปัญหาที่มีขอบเขตจำกัด วิธีนี้ค่อนข้างได้ผล และผมสามารถให้ AI สร้าง implementation ที่ครบถ้วนและทนทานกว่าที่ผมเขียนเองได้
      ต่อให้ภายหลังตัดสินใจเพิ่มไลบรารี ก็จะเกิดการ encapsulate ที่เป็นธรรมชาติ แค่เปลี่ยน implementation ของฟังก์ชันของผมให้ใช้ไลบรารีก็พอ ไม่จำเป็นต้องไปแก้ทุกจุดที่เรียกใช้ และเมื่อถึงเวลาก็ยังทดลองหลายไลบรารีได้ง่าย
  • น่าสนใจทีเดียวในเชิงประชด Armin คือผู้เขียนดั้งเดิมของเว็บเฟรมเวิร์ก Python อย่าง Flask ในช่วงเวลาใกล้กันก็มีไลบรารีที่คล้ายกันมากชื่อ Bottle
    ฟีเจอร์แทบจะเหมือนกัน แต่ Flask ได้รับความนิยมอย่างมาก ขณะที่ผมชอบ Bottle เสมอ เพราะเป็นไฟล์เดียวและไม่มี dependency จึงคัดลอกใส่โปรเจกต์ได้ง่ายมาก
    แก้ดัดแปลงก็ง่าย และท้ายที่สุดผมก็เข้าใจมันได้ทั้งตัว สำหรับ server และ websocket ผมเอา Gevent มาต่อเพิ่ม แต่ด้วยวิธีนั้นก็ยังสร้างโปรเจกต์ที่ค่อนข้างหนักได้
    ทุกวันนี้ผมยังมีแรงกระตุ้นแรงมากที่จะใช้ Bottle กับโปรเจกต์เว็บขนาดเล็ก เพียงแต่น่าเสียดายที่มันตามแนวปฏิบัติสมัยใหม่หลายอย่างที่ Python นำเข้ามาในช่วงหลายปีไม่ทัน จึงดูเก่าไปบ้างแล้ว

  • ถ้าจะสร้างเอง ต้องมีความสามารถด้านวิศวกรรมที่เก่งพอ หากมีแต่วิศวกรที่เคยหยิบไลบรารีจาก ecosystem อย่าง NPM หรือ PyPI มาใช้ตลอด ก็จะลำบากในการพัฒนาโซลูชันของปัญหาหลายอย่างด้วยตัวเอง โดยเฉพาะถ้าต้องเป็นโซลูชันที่อยู่ได้นานและมีความยืดหยุ่นตามที่ต้องการ
    “การไม่เขียนโปรแกรมพาตัวเองไปสู่ทางตัน” ต้องอาศัยการฝึกฝนมาก
    ผมเห็นบ่อยเหมือนกันว่ามีกรณีที่ทำให้ดีกว่าไลบรารีเดิมได้ง่าย ๆ ในโปรเจกต์หนึ่ง ผม implement parser สำหรับ Markdown variant ที่มี metadata อยู่ด้านบนไฟล์ ใช้ไวยากรณ์เล็ก ๆ และได้ parser ด้วยโค้ดไม่ถึงหนึ่งหน้าจอ
    แต่ไลบรารีฝั่ง frontend ที่ parse ไฟล์เดียวกันกลับแย่กว่าที่คาดมาก ถ้า metadata identifier มีขีดกลางก็พัง และปรากฏว่ามันใช้ metadata identifier เป็นชื่อ member ของ object โดยตรง แทนที่จะใช้ JSON object ธรรมดา กลับจำกัดการเลือกชื่อขึ้นมาเอง และทำให้พังถ้ามีขีดกลางอย่าง something-something
    สุดท้าย parser ของผมถูกทิ้ง เหตุผลคือผู้คนต้อง “ดูแลรักษา” มัน แต่จริง ๆ มันก็แค่ทำงานได้ และปรับตามการเปลี่ยนไวยากรณ์ได้ง่าย หากรู้เรื่อง parser เพียงเล็กน้อยก็ไม่ได้เข้าใจยากอะไร
    ไม่น่าเชื่อ แต่ดูเหมือนว่าในทีมจะไม่มีใครนอกจากผมที่เคยเขียน parser ด้วยไลบรารี parser generator มาก่อน ยังมีตัวอย่างแบบนี้อีกมาก

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

    • ใน Rust ไลบรารีสามารถกำหนด ฟีเจอร์ (features) ที่เปิดปิดแบบมีเงื่อนไขได้ จึงมีวิธีในตัวสำหรับควบคุมขอบเขตที่จะถูกรวมเข้ามาจริง ๆ Tokio เป็นตัวอย่างที่ดี และคุณอาจแปลกใจที่ dependency โดยตรงที่ Tokio ต้องใช้จริง ๆ มีทั้งหมดแค่สองตัวเท่านั้น ที่เหลือเป็นตัวเลือกทั้งหมด https://github.com/tokio-rs/tokio/blob/ee19b0ed7371b069112b9...
      น่าเสียดายที่ดูเหมือนผู้คนไม่ได้ตรวจดูชุดฟีเจอร์เริ่มต้นที่ dependency ใช้กันอย่างละเอียดและลดมันลงอย่างจริงจัง ไวยากรณ์สำหรับเพิ่มฟีเจอร์นั้นเรียบง่าย แต่ถ้าจะเอาฟีเจอร์เสริมที่เปิดไว้โดยค่าเริ่มต้นออก ต้องระบุ no-default-features แล้วก็ยังต้องเพิ่มฟีเจอร์เริ่มต้นที่ต้องการกลับเข้าไปทีละตัว จึงยุ่งยากกว่า
      ที่แย่กว่านั้นคือ หากอยากให้ไลบรารีสามารถตัดฟีเจอร์ที่ไม่จำเป็นออกจาก dependency ของตัวเองได้ ก็ต้องสร้างฟีเจอร์ของตัวเองขึ้นมาและแมปไปยังฟีเจอร์ของแต่ละ dependency ตัวอย่างเช่น ถ้ามี dependency 5 ตัว และแต่ละตัวมี dependency ที่จำเป็น 1 ตัวกับ dependency แบบเลือกได้ 4 ตัว หากต้องการให้ผู้ใช้ควบคุมฟีเจอร์ที่ส่งต่อมาได้อย่างสมบูรณ์ ก็ต้องสร้างฟีเจอร์สำหรับแมป 20 ตัวในไลบรารีของตัวเอง ยังไม่นับฟีเจอร์ที่จะทำขึ้นเพื่อเอื้อผู้ใช้ปลายน้ำสำหรับโค้ดของตัวเองอีก
      ผมเริ่มมองว่าความสะดวกในการใช้งานรอบ ๆ ฟีเจอร์ โดยเฉพาะในด้านการลดความเทอะทะที่ไม่จำเป็นนั้น แย่เกินไปจนกลายเป็นตัวเร่งปัญหาเวลา compile ของ Rust ดูเหมือนหัวข้อนี้จะไม่ค่อยถูกพูดถึงในวงกว้างแม้จะถูกยกขึ้นมา ผมอาจต้องเขียนความเห็นแรง ๆ ของตัวเองเป็นบล็อกโพสต์ไว้ เพื่อจะได้มีอะไรให้ชี้ไปหาเมื่อสถานการณ์ปัจจุบันยังดำเนินต่อไป
    • ถ้าทำแบบนั้นก็จะเกิดเรื่องแบบ leftpad ใน ecosystem ของ JS
      ผมคิดว่าอีกด้านหนึ่งมันไม่ใช่ปัญหา ถ้าระบบความเชื่อใจดีพอ
      ผมก็ชอบแนวทางแบบ Shadcn อยู่บ้าง คือไอเดียที่ว่าถ้าชอบคอมโพเนนต์ไหนก็ให้คัดลอกเข้าไลบรารีของตัวเอง แต่ถ้าเกิดช่องโหว่ขึ้นมา ผมจะไม่รู้ว่าตัวเองได้รับผลกระทบหรือไม่
      สำหรับโค้ดบางแบบก็โอเค แต่สำหรับโค้ดอื่น ๆ จำเป็นจริง ๆ ที่ต้องพึ่งพาสิ่งที่มีคนจำนวนมากที่สุดคอยตรวจดู
    • สำหรับคนที่ไม่รู้ ถ้าเขียนเป็น pseudocode ก็ประมาณนี้
      Median(list) { let len = length(list) if len % 2 == 0 { let x = floor(len/2) return (list[x] + list[x+1]) / 2 } return list[len/2] }
      โดยสมมติว่าลิสต์ถูกเรียงลำดับไว้แล้ว ตรงนี้ผมหักห้ามใจไม่เรียก is_odd แล้ว