3 คะแนน โดย GN⁺ 2023-10-26 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • หลังจากย้ายจาก JavaScript ไปใช้ Rust เพื่อโฟกัสกับ WebAssembly ตลอด 3 ปีที่ผ่านมา ได้สร้าง Wick, การ deploy บน production, ebook และแพ็กเกจบน crates.io ประมาณ 100 รายการ พร้อมประเมินคุณค่าจริงของ Rust
  • borrow checker, ระบบ type ที่สมบูรณ์, รูปแบบ functional, และการไม่มี null ช่วยกันข้อผิดพลาดจำนวนมากตั้งแต่ขั้น compile ทำให้ดูแล codebase ขนาดใหญ่ด้วย test น้อยลง ได้
  • Clippy และ Cargo workspace ทรงพลัง แต่ ช่องว่างของเครื่องมือและ ecosystem เช่น การตั้งค่า lint แบบ global และการ deploy workspace กลายเป็นต้นทุนในการปฏิบัติการ
  • async, การ refactor, การจัดการ generic·lifetime·trait constraint ยังคงเป็น พื้นที่ที่มีแรงเสียดทานสูง กว่า JavaScript หรือ Go
  • Rust แข็งแกร่งและใช้ได้หลากหลาย แต่มีต้นทุนสูงด้านการจ้างงาน การเรียนรู้ การ iterate อย่างรวดเร็ว และการตามรอยปัญหา จึงเหมาะกว่าเมื่อ scope ชัดเจนหรือรับต้นทุนเริ่มต้นได้

WebAssembly เป็นปัจจัยที่นำไปสู่การเลือก Rust

  • หลายปีก่อน ผู้เขียนวางงานเดิมลงเพื่อโฟกัสกับ WebAssembly 100% และในเวลานั้น Rust รองรับการ compile ไป WebAssembly ได้ดีที่สุด
  • WebAssembly runtime ที่มีฟีเจอร์ครบถ้วนจำนวนมากก็สร้างบน Rust ทำให้ Rust เป็นตัวเลือกที่สมจริงที่สุดในบรรดาตัวเลือกทั้งหมด
  • หลังจากนั้นได้สร้าง Wick ซึ่งเป็นทั้ง application framework และ runtime ที่ใช้ WebAssembly เป็นระบบ module หลัก
  • ตลอด 3 ปี ได้สะสมประสบการณ์ Rust ผ่าน การ deploy บน production หลายครั้ง, ebook, และการเผยแพร่ แพ็กเกจประมาณ 100 รายการ บน crates.io

ดูแลโค้ดได้มากขึ้นด้วย test ที่น้อยลง

  • ใน Rust ตอนแรกก็เขียน test แบบเดียวกับภาษาอื่นทั่วไป แต่พบว่ากำลังเขียน test สำหรับสิ่งที่ไม่มีทางล้มเหลวได้หาก compile ผ่านแล้ว
  • หากหลีกเลี่ยง unsafe {} block และเมธอดที่เสี่ยง panic อย่าง .unwrap() ปัญหาจำนวนมากก็จะถูกหลีกเลี่ยงโดยพื้นฐาน
  • borrow checker, ระบบ type ที่สมบูรณ์, รูปแบบและไลบรารีแนว functional, และการไม่มีค่า null ช่วยลดความพยายามที่ต้องใช้กับ test
  • โปรเจกต์ Wick ดูแล โค้ดมากกว่า 70,000 บรรทัด ได้ด้วย test น้อยกว่าที่น่าจะต้องใช้ในภาษาอื่นมาก
  • เมื่อจำเป็นต้องมี test ก็สามารถเพิ่มไว้ข้างโค้ดได้ง่ายด้วย integrated test harness ของ Rust

Rust เปลี่ยนนิสัยการเขียนโค้ดในภาษาอื่นด้วย

  • compiler ของ Rust มักแสดงความไม่พอใจต่อโค้ดที่ในภาษาอื่นเคยมองว่าปกติ และผ่านกระบวนการนั้นไปเรื่อย ๆ นิสัยการเขียนโค้ดก็เปลี่ยนไป
  • ตอนนี้แม้เขียนภาษาอื่นก็รู้สึกไม่สบายใจเมื่อ order ของบรรทัดโค้ดดูแปลก หรือไม่ได้ตรวจสอบค่าที่ return
  • เมื่อเจอ runtime error ก็รู้สึกต่อต้านมากกว่าเดิมมาก
  • ความเข้มงวดของ Rust อาจไม่สะดวก แต่เมื่อคุ้นเคยกับประสบการณ์ที่ compiler ช่วยปกป้องแล้ว ก็ยากที่จะกลับไปใช้ภาษาอื่น

Clippy มีประโยชน์มากกว่า linter

  • Clippy เป็น linter ของ Rust แต่ใกล้เคียงกับ ผู้ช่วยที่เป็นมิตร ซึ่งเสนอ code ทางเลือก มากกว่าเป็นเพียงเครื่องมือตรวจสอบ
  • standard library ของ Rust มีขนาดใหญ่มาก และกระจายอยู่ใน type, trait, macro, function ที่มีฟีเจอร์จำนวนมาก ทำให้หา API ที่ต้องการได้ยาก
  • rule หลายรายการสามารถค้นหา pattern ทั่วไปที่แทนที่ได้ดีกว่าด้วยเมธอดหรือ type ใน standard library
  • rule หลายร้อยรายการ ครอบคลุม performance, readability, indirect ที่ไม่จำเป็น และให้ code ทางเลือกด้วยเมื่อทำได้
  • การตั้งค่า lint ระดับทั้งโปรเจกต์ดูเหมือนจะทำได้ผ่าน Cargo issue แต่ก่อนหน้านั้น Wick ต้องอัปเดต inline lint setting ของ crate หลายสิบรายการโดยอัตโนมัติด้วยสคริปต์

ecosystem ยังมีช่องว่างที่ต้องยอมรับ

  • ปัญหาการตั้งค่า Clippy แบบ global เป็นตัวอย่างหนึ่งของ ช่องว่างใน ecosystem ที่มักพบในเครื่องมือและไลบรารีของ Rust
  • issue ที่เกี่ยวข้องปัจจุบันปิดไปแล้ว แต่เคยเปิดอยู่หลายปีและใช้เวลานานกว่าจะได้รับการแก้ไข
  • Rust ดึงดูดผู้ใช้ใหม่ได้มากจนถูกเลือกเป็น “ภาษาที่เป็นที่รักที่สุด” มานาน แต่กระแสนั้นไม่ได้แปรเปลี่ยนโดยตรงเป็นการปรับปรุงไลบรารีและเครื่องมือแบบก้าวกระโดด
  • บ่อยครั้งจะเกิด fork แบบครั้งเดียวเพื่อรองรับ use case เฉพาะ และ Wick ก็เคยเจอสถานการณ์คล้ายกันตอนพยายามส่ง PR
  • เหตุผลที่เป็นไปได้คือแรงกดดันในการรักษา API ที่ stable และระบบ type ที่ละเอียดมาก
    • เจ้าของไลบรารีอาจรับการเปลี่ยนแปลงเล็ก ๆ ได้ยาก เพราะอาจนำไปสู่การเปลี่ยน major version
    • ภาระในการเขียนโค้ด Rust ที่ตอบสนองความต้องการของทุกคนก็สูงเช่นกัน

แรงเสียดทานของ Cargo, crates.io และการ deploy workspace

  • โครงสร้าง repository ของ Wick สร้างโดยอ้างอิงจากโปรเจกต์ยอดนิยม และตอนแรกก็ดูสมเหตุสมผล แต่ปัญหาเผยตัวใน ขั้นตอน deploy
  • การ build, test และใช้งาน crate ขนาดระดับ module ด้วย Cargo เป็นเรื่องง่าย แต่การ deploy ไป crates.io เป็นอีกเรื่องหนึ่ง
  • บน crates.io ต้องมี crate ทุกตัวที่อ้างอิงถูก deploy แยกไว้แล้ว จึงจะ publish package ได้
    • การป้องกันไม่ให้ deploy crate ที่พึ่งพา package ซึ่งมีอยู่เฉพาะบน filesystem ในเครื่องนั้นเป็นเรื่องสมเหตุสมผล
    • แต่ในโครงสร้างตามธรรมชาติของโปรเจกต์ใหญ่ที่แบ่งเป็น module ภายในขนาดเล็ก จะไม่สามารถ publish พร้อม sub-crate ที่มีอยู่เฉพาะใน parent crate ได้
  • มีการแก้ไขข้อมูลว่า crate ที่มี local dev dependency สามารถ publish ได้หากใส่ version ไว้ใน Cargo.toml
  • การรองรับ Cargo workspace เองนั้นยอดเยี่ยม และประสบการณ์การจัดการโปรเจกต์ขนาดใหญ่นั้นดีกว่าภาษาส่วนใหญ่
  • อย่างไรก็ตาม workspace ไม่ได้แก้ปัญหาการ deploy และแม้จะมีวิธีตั้งค่าหลายแบบ ก็ยังยากที่จะพบ “คำตอบที่ถูกต้อง” ที่ deploy ได้ง่าย
  • การมี utility crate ที่เกี่ยวกับ cargo workspace publish อยู่จำนวนมาก เป็นสิ่งที่สะท้อนปัญหาได้ในตัวเอง
  • ตอน publish Wick มักต้องใช้เวลา มากกว่า 1 ชั่วโมง เพราะต้องผสมผสานงานซ้ำ ๆ แบบ manual กับเครื่องมือที่ทำงานได้เพียงบางส่วน

async เป็นหนึ่งในจุดเสียดทานที่ใหญ่ที่สุด

  • async ของ Rust ให้ความรู้สึกเหมือนเป็นฟีเจอร์ที่ถูกเพิ่มหลังจากภาษาออกแบบมาแล้ว และในการใช้งานจริงก็มักขัดจังหวะเหมือนฟีเจอร์ที่ถูกแปะเพิ่มภายหลัง
  • error เข้าใจและแก้ไขได้ยาก และตอนค้นหาวิธีแก้ก็ต้องกรองตาม runtime หลายตัวกับแนวทาง async ของแต่ละตัว
  • ไลบรารี async บางตัวอาจใช้งานนอก async runtime เฉพาะบางตัวไม่ได้
  • จากมุมมองของคนที่ใช้ JavaScript มา 20 ปีและมีประสบการณ์ Go ด้วย async ของ Rust เป็นแหล่งความหงุดหงิดและแรงเสียดทานที่ใหญ่ที่สุด
  • ไม่ใช่ปัญหาที่เอาชนะไม่ได้ แต่ต้องเตรียมพร้อมเสมอว่าปัญหา async อาจโผล่มาได้ทุกเมื่อ
  • ในภาษาอื่น async ทำงานได้เป็นธรรมชาติจนแทบมองไม่เห็น

การ refactor อาจเป็นงานหนัก

  • ระบบ type ที่สมบูรณ์ของ Rust เป็นทั้งข้อดีและข้อเสีย
  • การคิดด้วย type ของ Rust เป็นเรื่องดี แต่การจัดการ type ของ Rust อาจกลายเป็นฝันร้าย
  • ข้อมูลและ function signature สามารถมี generic type, generic lifetime และ trait constraint ได้
  • ตัว constraint เองก็สามารถมี generic type และ lifetime ได้อีก ทำให้บางครั้งมี type constraint มากกว่าโค้ดจริง
  • ต้องกำหนด generic สำหรับทุก impl จึงยุ่งยากตั้งแต่ตอนเขียนครั้งแรก และเวลา refactor การเปลี่ยนแปลงเล็ก ๆ อาจลามเป็นการแก้ไขต่อเนื่องจำนวนมาก
  • เมื่อต้องทำซ้ำ constraint หรือรายการ generic เดิมในหลายที่ ยังไม่มีวิธีในระดับภาษาและเครื่องมือที่จะ alias หรืออ้างอิงจากนิยามกลางได้ จึงยังมี ภาระจากความซ้ำซ้อน อยู่

ข้อสรุป: ทรงพลังแต่ต้นทุนสูง

  • Rust ใช้งานได้หลากหลายพอที่จะเขียน system-level code, CLI app, web server และ web client ด้วยภาษาเดียวกัน
  • เมื่อใช้ WebAssembly สามารถรัน LLM ด้วย binary เดียวกันได้ทั้งใน browser และ command line
  • โปรแกรม Rust สามารถแข็งแกร่งมาก และเมื่อสัมผัสได้ถึงปัญหาที่ Rust ช่วยกันไว้ให้แล้ว ก็ยากที่จะกลับไปใช้ภาษาอื่น
  • ตอนกลับไปใช้ Go ชั่วคราว ความเร็วในการพัฒนากลับมาน่าดึงดูดอีกครั้ง แต่หลังเจอ runtime panic ข้อดีก็เริ่มสั่นคลอน
  • Rust มีข้อเสียที่ชัดเจน
    • จ้างคนยาก
    • เรียนรู้ช้า
    • แข็งเกินไปสำหรับการ iterate อย่างรวดเร็ว
    • โดยเฉพาะใน async code การตามรอยปัญหา memory และ performance ทำได้ยาก
    • ไม่ใช่ทุกไลบรารีจะดีพอสำหรับโค้ดที่ปลอดภัย
    • เครื่องมือพัฒนายังมีพื้นที่ให้ปรับปรุงอีกมาก
  • แม้ทีมเล็กจะทำสิ่งน่าทึ่งได้ แต่ก็มีอุปสรรคใหญ่ และมีเหตุผลทางเทคนิคที่ทำให้ Rust เหมาะสมกว่า ดังนั้นจึงยังเร็วเกินไปที่จะตัดสินว่า Rust คุ้มค่าสำหรับ Wick หรือไม่
  • หากต้อง iterate อย่างรวดเร็ว Rust มีแนวโน้มสูงว่าอาจไม่เหมาะ
  • หาก scope เป็นที่รู้แล้ว หรือสามารถรับต้นทุนเริ่มต้นที่สูงกว่าได้ Rust ก็คุ้มค่าที่จะพิจารณาอย่างจริงจัง
  • มุมมองด้าน WebAssembly แข็งแรงขึ้นทุกเดือน ทำให้ความเป็นไปได้ในการนำซอฟต์แวร์ที่แข็งแกร่งซึ่งเขียนครั้งเดียวไปใช้ซ้ำในหลายที่ เข้าใกล้ความจริงมากขึ้น

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

 
GN⁺ 2023-10-26
ความคิดเห็นบน Hacker News
  • ใช้ Rust มามาก แต่ผ่านไปหลายปีก็ยังรู้สึกว่า ประสิทธิภาพในการทำงานต่ำ
    ช่วงนี้ใช้ Zig เยอะ และรู้สึกว่าทำงานได้มีประสิทธิภาพกว่าราว 10 เท่า เพราะโฟกัสกับการเขียนโค้ดที่อยากเขียนได้เลย ไม่ต้องคอยคิดว่าจะใช้เครื่องมือหรือไลบรารีอะไร
    รู้ว่า Rust ให้ ความปลอดภัยของหน่วยความจำ และสิ่งนั้นสำคัญ แต่การใช้งานแย่มาก ทุกครั้งที่ใช้ Rust จะรู้สึกถูกจำกัด และต้องคอยหาไลบรารีหรือค้นหาวิธีทำงานอยู่เสมอ จนไม่สามารถแค่ “พิมพ์โค้ด” ได้
    ระบบ type ก็อาจขยายจนเหมือนควบคุมไม่อยู่ และหลายครั้งก็ยากจะรู้ว่าใน struct หนึ่ง ๆ เรียกใช้เมธอดอะไรได้จริง Rust เป็นเครื่องมือที่ยอดเยี่ยมและแก้ปัญหาได้มากมาย แต่ไม่คิดว่าเป็นภาษาอเนกประสงค์ที่ดี

    • ความรู้สึกแบบนั้นไม่ได้เหมือนกันทุกคน ถ้าเป็นงานที่ซับซ้อนกว่า shell script นิดหน่อยก็ใช้ Rust และแม้แต่ window manager ของผมก็ยังควบคุมด้วยโปรแกรม Rust
      จากมุมของคนที่ใช้ Python มาเกือบ 20 ปี ตอนนี้ผมทำงานด้วย Rust ได้เร็วพอ ๆ กับ Python แล้ว
    • เห็นด้วย ตอนนี้รู้สึกว่าทำงานได้มีประสิทธิภาพกว่าใน C และ C++ มากกว่า Rust
      สำหรับผม Rust เหมือนพลาดจุดสมดุลที่เหมาะสมไปโดยสิ้นเชิง จะเขียนแอปพลิเคชันระดับสูงก็จุกจิกกับรายละเอียดระดับต่ำมากเกินไป จะเขียน embedded หรือระบบปฏิบัติการก็ซับซ้อนเกินไป
      ถ้าเป็นกรณีแรกคงเลือก C++, Java, Haskell, OCaml หรือแม้แต่ Go ผสม C นิดหน่อย ส่วนกรณีหลัง C ที่ใช้เหมือน macro assembly เหมาะกว่ามาก
      ยังรู้สึกอยู่ว่าแนวคิดเดิมของ Graydon Hoare คือภาษาทางสาย OCaml/SML ที่มี linear type, garbage collection, stack allocation, green thread และ CPS น่าจะกลายเป็นภาษาที่ดีกว่ามาก
    • ประสบการณ์ของผมตรงกันข้ามเลย การใช้ Rust ใน ระบบ embedded ทำให้ความมั่นใจและความเร็วดีขึ้นอย่างมาก
      ใน C ความผิดพลาดเล็กน้อยเพียงอย่างเดียวมักนำไปสู่ undefined behavior และเรื่องปวดหัว แต่ใน Rust ไม่มีสิ่งนั้น จึงเปลี่ยนเกมไปโดยสิ้นเชิง
    • อยากรู้ว่าเขียนโค้ดประเภทไหน เป็นระดับต่ำมาก ๆ หรือระดับสูงมาก ๆ?
      สงสัยว่าหมายถึงใน Zig ไม่จำเป็นต้องหาไลบรารีหรือหาวิธีทำเลยหรือเปล่า
    • เห็นด้วยกับความรู้สึกโดยรวม แต่ยากที่จะอธิบายเป็นคำพูด
      Rust บังคับให้ตัดสินใจล่วงหน้าว่าแต่ละบิตและไบต์จะไปอยู่ที่ไหน ใช้ใน thread ไหน และถูกจัดการด้วยรูปแบบการเปลี่ยนแปลงแบบใด ถ้าไม่ใช่ระดับ parser หรือ microcontroller กระบวนการนี้จะรู้สึกน่าเบื่อ
      ผมชอบทำให้บางอย่างทำงานได้ก่อน แล้วค่อยกำหนดโครงสร้าง API ที่ดีที่สุด แต่ Rust ขัดกับกระบวนการนั้น
      แม้ระบบ type ของ Rust จะแข็งแกร่งกว่า แต่ Swift ก็ให้ประสิทธิภาพได้ 90% และไหลลื่นเป็นธรรมชาติกว่ามาก
  • การที่ crates.io ไม่มี namespace อาจเป็นจุดวิจารณ์ที่ใหญ่ที่สุด
    ใครก็สามารถจองชื่อแพ็กเกจทั่วไปในระดับ global ไว้ก่อนได้ และถ้าไม่เลี่ยง repository ของ crates.io ส่วนใหญ่ก็ต้องยอมรับเรื่องนี้ แต่แพ็กเกจที่จองชื่อทั่วไปบางตัวไว้กลับไม่ใช่แพ็กเกจที่ดีที่สุดสำหรับการใช้งานจริง
    อาจเป็นปฏิกิริยาต่อต้านรูปแบบ reverse DNS แบบ Java ที่ยืดยาวและน่ารำคาญ แต่การใส่ namespace ของผู้ใช้/กลุ่มไว้หน้าชื่อแพ็กเกจแบบ GitHub น่าจะเป็นจุดกึ่งกลางที่ดี

    • เคยทำการวิเคราะห์เพื่อหา ผู้จองชื่อ บน crates.io และคำนวณได้ว่าผู้จองชื่ออันดับต้น ๆ สร้าง crate ประมาณหนึ่งตัวทุก ๆ 30 วินาทีตลอดทั้งสัปดาห์
      ส่งการวิเคราะห์นั้นให้ทีม crates.io แล้ว และยังชี้ให้เห็นว่ามีนโยบายห้าม automation ด้วย แต่พวกเขาตอบว่ายังไม่ใช่หลักฐานเพียงพอว่าเป็นการจองชื่อ
      ปัญหาของ crates.io คือแม้จะมีนโยบายชัดเจนก็ไม่บังคับใช้ ดังนั้นชื่อ crate ที่สั้นและจำง่ายจึงถูกยึดไปหมดแล้ว และไม่มีทางเอากลับมาได้
    • ดูเหมือนไม่ใช่ปฏิกิริยาต่อต้าน reverse DNS แบบ Java เท่าไร แต่น่าจะเป็นการทำตามแนวปฏิบัติที่ package manager ส่วนใหญ่ตั้งไว้ในตอนนั้นมากกว่า
      NPM, PyPI, RubyGems, Hex ของ Elixir, Cabal ของ Haskell ฯลฯ ช่วงราวปี 2014–2015 ที่ Rust ออกมา แทบนึกตัวอย่าง package manager ที่ไม่ใช่ Java และไม่ได้ใช้ namespace ระดับ global เดียวไม่ออก
      บางแห่งพยายามแก้เรื่องนี้ภายหลัง แต่ตอนนั้น package manager ก็ทำงานกันแบบนั้น
    • Maven และ Java ไม่ได้รับการยอมรับเท่าที่ควรในแง่ที่ระบบจัดการ dependency ทำงานได้ดี
      ระบบจัดการ dependency ที่แย่กว่าของภาษาอื่น ๆ ที่ออกมาภายหลังแทบไม่ได้เรียนรู้อะไรจากตัวอย่างก่อนหน้าเลย
    • วิธีใช้ URL สำหรับแพ็กเกจก็ค่อนข้างสมเหตุสมผล ใช้ได้ดีใน ecosystem ของ Go
      ข้อดีคือไม่จำเป็นต้องมีฐานข้อมูลแพ็กเกจระดับ global ในระดับภาษาอีกต่อไป ถ้าเอาแพ็กเกจไปไว้ที่ example.com/your-thing ก็เหมือน release ได้ทันที
      แน่นอนว่าถ้าต้องการก็สามารถจัด cache และ search engine แยกต่างหากได้
    • ไม่ได้ใช้ Rust บ่อยนัก แต่ปัญหาแบบนี้น่ารำคาญมากใน repository แพ็กเกจโดยรวม
      เหมือนกับว่า http-server แย่ อย่าใช้ ให้ใช้ MuffinTop แทน ซึ่งคุณต้องรู้อยู่เอง
      แนวคิดเรื่องชื่อแพ็กเกจที่ได้รับการรับรองนั้นน่าสนใจ แต่เมื่อเวลาผ่านไป ถ้าโค้ดหลัง alias เปลี่ยนไปจริง ๆ ก็น่าจะทำให้สับสนได้มาก
      ท้ายที่สุดมันคงยังเป็นส่วนหนึ่งของกระบวนการกลายเป็นผู้เชี่ยวชาญโดเมนใน ecosystem ใด ๆ ต่อไป
  • ถ้าสร้าง .cargo/config.toml ที่ root ของ workspace จะถูกใช้กับทุก crate จึงสามารถตั้งค่า Clippy lint ระดับ global ได้
    ในไฟล์ให้ใส่ไว้ใต้ [build] เช่น rustflags = ["-Wclippy::lint_name_to_warn", "-Dclippy::lint_name_to_deny"]
    อย่างไรก็ตาม rustflags ไม่ได้ทำงานแบบเพิ่มต่อท้าย แต่เป็นการเขียนทับ ดังนั้นถ้ามีแหล่งอื่นอย่างตัวแปรสภาพแวดล้อม RUSTFLAGS ก็จะเขียนทับการตั้งค่านี้

    • เราเรียกสคริปต์ตอน commit เพื่อเพิ่ม lint ลงในไฟล์ lib.rs หรือ main.rs เรียบง่ายดี
  • กำลังเรียน Rust เพราะเห็นชัดว่ามันจะมีความสำคัญในงานอาชีพ อยากชอบมันจริง ๆ และก็เห็นข้อดีอยู่ แต่จนถึงตอนนี้มันเข้าข่ายเป็นหนึ่งในภาษาที่ ทำให้รู้สึกไม่สบายใจที่สุด เท่าที่เคยใช้มา
    ผมหวังมาตลอดว่าพอชำนาญขึ้นแล้วความรู้สึกไม่ชอบจะหายไป แต่ยิ่งไต่เส้นโค้งการเรียนรู้ขึ้นไป ก็ไม่ได้รู้สึกผูกพันกับมันมากนัก
    ไม่เป็นไรหรอก นี่คงไม่ใช่ภาษาเดียวที่ผมใช้ได้คล่องแต่ไม่ชอบ เพียงแต่มีคนจำนวนมากบอกว่ารัก Rust ผมเลยคิดว่าตัวเองก็น่าจะสนุกกับมันได้

    • ในฐานะตัวอย่างโต้แย้ง ผมชอบ การเขียนโปรแกรม Rust นะ ช่วงที่ต้องสู้กับ borrow checker จบไปนานแล้ว และเดี๋ยวนี้นอกจากกรณีที่ตั้งใจทำให้เกิดเพื่อเช็กชนิดข้อมูล ก็แทบไม่เจอข้อผิดพลาด
      Rust compiler เองก็ดูเหมือนจะดีขึ้นในการยอมรับกรณีที่ถูกต้องในวงกว้างมากขึ้น
      หัวใจของการเข้าใจ borrow checker คือการเข้าใจ memory model ที่อยู่เบื้องหลัง memory model ของ Rust เหมือนกับ C ยกเว้นส่วนขยายเพื่อรองรับ abstraction อย่าง generics
      กฎของ borrow checker ตอนแรกอาจดูเหมือนกำหนดขึ้นตามอำเภอใจ แต่จริง ๆ แล้วเชื่อมโยงอย่างลึกซึ้งกับ memory model นี้ คุณค่าที่แท้จริงคือเวลาที่เราไปติด borrow checker โดยไม่ตั้งใจ เพราะนั่นคือบั๊กที่เกิดจากความเผลอเรอ
      สิ่งที่น่ากลัวคือในภาษาอย่าง C หรือ C++ โค้ดแบบนั้นอาจถูกยอมรับแล้วปล่อยให้เดินหน้าต่อไป
      ระบบชนิดข้อมูลที่เข้มงวดและ borrow checker ของ Rust จะค่อย ๆ ผลักให้เราออกแบบโครงสร้างโค้ดอย่างถูกต้อง และผมมั่นใจว่ามันช่วยปรับปรุงการออกแบบโค้ดของผมในทุกภาษาที่ใช้
    • พยายามเรียน Rust มาหลายครั้งแล้ว แต่ก็เด้งออกมาตลอด รู้สึกว่ามันแค่ ใช้งานไม่ค่อยดี
    • สิ่งที่ยังติดใจคือไม่มี ค่าเริ่มต้น/อาร์กิวเมนต์ฟังก์ชันแบบมีชื่อ
      นี่เป็นฟีเจอร์ด้าน usability พื้นฐานมากสำหรับโปรแกรมเมอร์ที่ภาษาได้รับความนิยมส่วนใหญ่มีร่วมกัน แม้แต่ C++ ก็มี default arguments มานานแล้ว
    • อยากรู้ว่าไม่ชอบตรงไหน
  • ผมเป็นอดีตโปรแกรมเมอร์ C/C++ ที่เรียกใช้ pthread แทบทุกสัปดาห์ตลอด 10 ปี ตอนนี้ใช้ async Rust ไปทุกที่
    ไม่เข้าใจว่าทำไม async ถึงถูกเกลียดขนาดนั้น ในความคิดผม ทุกคนควรใช้ async กับทุกอย่าง รวมถึงงาน “ง่าย ๆ” ที่ดูเหมือน single-threaded ด้วย

    • ผมมองว่าปัญหาคือมัน แพร่ลาม โดยเฉพาะในสภาพแวดล้อม embedded หรือ Wasm async กระแสหลักอาจไม่ใช่ async แบบที่ผมต้องการ
      ถ้า use case ของผู้เขียนเป็น Wasm ก็คงได้มุมมองอีกแบบแน่นอน
      งานที่ใช้บัฟเฟอร์ขนาดใหญ่หรือ reuse เพื่อหลีกเลี่ยงต้นทุนการ allocate ก็มักได้ประโยชน์จาก thread pool แบบเก่า ๆ อยู่เหมือนกัน bump allocator หรือ sharded allocator ช่วยได้ในระดับหนึ่ง แต่ถ้าเป็นลูปแน่น ๆ ที่ vectorize ได้และติด CPU-bound แล้ว thread pool มักทำงานได้ดีกว่า
      async เป็นเครื่องมือที่ดี แต่ไม่ได้มีแต่บริบทที่เหมาะที่สุดเสมอไป
    • เมื่อไม่นานมานี้ลองใช้ async Rust นิดหน่อยเพื่อเทียบว่าการจัดการ error ในการเรียก async ซ้อนกันต่างจาก Go อย่างไร แต่ดูเหมือนว่าแม้แต่ตัวอย่างเล็กน้อยที่กำลังจะเขียนก็ทำไม่ได้ถ้าไม่มี Tokio
      Go, C#, TypeScript ไม่มีอุปสรรคแบบนั้น
      เช่น ดูเหมือนว่าถ้าไม่มี decorator ที่ import มาจาก Tokio ก็ await ในฟังก์ชัน main ไม่ได้ด้วยซ้ำ
    • ปัญหาที่เจอตอนนี้คือบางอย่างอย่าง rhai Engine ไม่ใช่ Send แต่ผมพยายามใช้มันใน async closure
      GPT-4 แนะนำให้สร้าง tokio Runtime ใน thread แล้วใช้ block_on() พรุ่งนี้ว่าจะลองดู นี่เป็นโปรเจกต์ Rust จริงจังโปรเจกต์แรกของผม
    • ผมกลับเรียนรู้มาตรงกันข้าม โปรแกรมเมอร์มีแนวโน้มทำทุกอย่างให้เป็น async โดยไม่จำเป็น เพิ่มความซับซ้อนและภาระทางความคิดโดยเปล่าประโยชน์
    • ช่วยอธิบายละเอียดกว่านี้ได้ไหม อยากรู้ว่าตัวอย่างงานง่าย ๆ ที่ดูเหมือน single-threaded แต่ควรเลือก async คืออะไร
      และก็สงสัยว่าคำว่า “ตอนนี้ใช้ async Rust ไปทุกที่” หมายถึง “ใช้ Tokio ไปทุกที่” หรือเปล่า
  • การเขียนโปรแกรม Rust ไม่เหมือนความสัมพันธ์แบบถูกทำร้ายจริง ๆ compiler พยายามช่วยเต็มที่ โดยเฉพาะ ข้อความ error ของ rustc อยู่ในระดับดีที่สุดของโลก

    • ระบบปฏิบัติการคาดหวังให้โปรแกรมเมอร์จัดการทรัพยากรอย่างถูกต้อง และ Rust compiler ก็ทำให้งานนั้นง่ายมาก
      สิ่งที่ใกล้เคียงกับความสัมพันธ์แบบถูกทำร้ายมากกว่าคือฝั่งระบบปฏิบัติการ ไม่ใช่ Rust compiler ฮาร์ดแวร์ก็เช่นกัน เพราะต้องรัน assembly ให้ถูกต้อง ถ้ามองแบบนั้นก็อาจเรียกได้ว่าเป็นความสัมพันธ์แบบถูกทำร้าย
      ข้อความ error ของ Rust นั้นโดดเด่น ไม่มี compiler ตัวไหนเข้าใกล้ได้เลย
      แถมล่าสุดผมใช้ LaTeX แล้วข้อความ error แย่มาก การต้องจ้อง error เพื่อหาว่าอะไรผิดพลาดเป็นฝันร้ายจริง ๆ
    • โดยรวมถือว่าดี แต่ผมเกลียดมากที่ error ใด ๆ ใน async function จะสร้าง error ว่า Future ไม่ใช่ Send และ Sync อีกต่อไปในทุกจุดเรียกซ้ำ
      ทั้ง console ถูก error ถมเต็มไปหมด และ syntax error จริง ๆ ถูกฝังอยู่ตรงไหนสักแห่งกลาง ๆ นั้น
    • Rust compiler เป็น compiler ตัวแรกที่ผมเคยเห็นที่ใช้คำว่า “perhaps”
      ข้อตำหนิหลักคือผมยังไม่เข้าใจ lifetime อย่างสมบูรณ์ และ compiler ก็ช่วยไม่ได้ทุกครั้ง พอเข้าใจได้ เพราะ compiler ต้องตัดสินแบบอนุรักษนิยม
  • ในการทดสอบ C++ มักมีคำพูดว่า “ถ้าคอมไพล์ผ่าน โดยมากก็ถูกแล้ว”
    ถ้าคุณคิดว่า Rust จัดการข้อผิดพลาดให้มากจนกรณีทดสอบทั่วไปกลายเป็นไม่มีความหมาย ผมมองว่านั่นเป็นสัญญาณว่าคุณไม่ได้ทดสอบสิ่งที่ถูกต้องในภาษาอื่น
    สิ่งที่ควรทดสอบไม่ใช่ปัญหาของตัวภาษาเอง แต่คือ business logic
    ถ้าดูโค้ดทดสอบแล้วรู้สึกว่า “ถ้าเป็น JavaScript ก็คงทดสอบ แต่ใน Rust ไม่ต้องก็ได้” ก็ลบทดสอบนั้นทิ้งได้เลย

    • คำว่า “ถ้าคอมไพล์ผ่าน โดยมากก็ถูกแล้ว” เคยได้ยินกับ Haskell และ Rust แต่ไม่เคยได้ยินว่าเอาไปใช้กับ C++
    • ข้อยกเว้นจาก null pointer คือบั๊กที่ทำลาย business logic
      ไม่มีการแบ่งแยกว่า “business logic เทียบกับปัญหาของภาษา” เพราะภาษาคือฐานที่ business logic ตั้งอยู่
      ถ้าไม่ทดสอบวิธีการล้มเหลว ก็ไม่รู้ว่าการทดสอบมีความหมายอะไร
    • มีความน่าทึ่งแปลก ๆ ในตัว โปรแกรมเมอร์ C++ ประเภทที่คิดว่า โค้ดขยะที่พังแล้วคอมไพล์ผ่าน ก็คงจะถูกแล้ว
      ต่างจากภาษาส่วนใหญ่ที่คุ้นเคย C++ มี IFNDR และถูกเรียกแบบติดตลกว่าเป็น false positive ต่อคำถาม “นี่เป็นโปรแกรม C++ หรือเปล่า?”
      คอมไพเลอร์ C++ ที่เป็นไปตามมาตรฐานถูกห้ามไม่ให้บอกในบางกรณีที่น่าสงสัยว่าโค้ดที่เขียนมานั้นไม่สมเหตุสมผล และต้องเดินหน้าต่อไปเพื่อพ่นอะไรบางอย่างออกมา
      นั่นอาจเป็นไฟล์รันได้ที่ทำงานได้ หรืออาจเป็นไฟล์รันได้ที่ก่อมหันตภัยทุกวันศุกร์ก็ได้ ไม่มีทางรู้
      มาตรฐาน ISO ระบุกรณีเหล่านี้ไว้ก็จริง แต่มันคลุมเครือมากจนยากจะจับให้ชัดว่ารวมอะไรบ้าง และผมเดาว่าซอฟต์แวร์ C++ ที่ไม่ trivial ส่วนใหญ่ในปัจจุบันน่าจะเป็น IFNDR จริง ๆ ทางที่ดีกว่าคือปฏิเสธภาษาทั้งหมดไปเลย
  • Rust ดูเหมือนจะทำลายความคิดที่ว่าโปรแกรมเมอร์ต้องควบคุมและรับรู้ทุกอย่างที่คอมไพเลอร์ทำได้ในที่สุด
    อันที่จริงมันไม่เป็นแบบนั้นมาตั้งหลายทศวรรษแล้ว และคอมไพเลอร์ก็แทบจะเหมือนเวทมนตร์ Rust ย้อนกระแสนั้นกลับไปมากด้วย borrowing ทำให้ผู้คนสบายใจกับการที่คอมไพเลอร์รู้ดีกว่าตัวเอง
    อยากให้สบายใจกว่านี้อีก ถ้าไม่ได้จำเป็นในเชิงอัลกอริทึม ก็ไม่ควรต้องวนซ้ำ collection จากหน้าไปหลังอย่างชัดเจน งานหลายอย่างควรถูก parallelize โดยปริยาย อยากได้ Bash แบบฉบับ Rust

    • ตรงข้ามกับประสบการณ์ Rust ของผมโดยสิ้นเชิง
      Rust ค่อนข้างโปร่งใสในสิ่งที่มันทำ และระมัดระวังมากกับ เวทมนตร์ของคอมไพเลอร์ ตัวภาษาไม่ทำ heap allocation, ไม่ทำ reference counting และไม่มีการแปลงชนิดตัวเลขโดยปริยาย
      ชนิดที่ไม่ได้ประกาศว่าคัดลอกได้โดยปริยายก็จะไม่ถูกคัดลอก และแม้แต่กรณีนั้นก็ถูกกฎหมายเฉพาะกับชนิดที่คัดลอกได้ด้วย memcpy แบบ shallow ง่าย ๆ เท่านั้น
      Rust ใช้ zero-cost abstraction อยู่ทั่วไป ดังนั้นจึงคาดเดาได้ว่าจะคอมไพล์ออกมาเป็นโค้ดแบบไหน และโดยปกติก็เรียบง่าย layout พื้นฐานของชนิดมาตรฐานก็เป็นที่รู้กันดี จึงรู้ได้ว่าการไล่ผ่าน Vec จะคอมไพล์เป็นลูปที่เพิ่ม pointer และไม่มี parallelism โดยปริยาย
      การอธิบาย borrowing ว่าเป็น “คอมไพเลอร์รู้ดีกว่าโปรแกรมเมอร์” ฟังดูแปลก borrowing คล้ายกับ type checking ถ้าประกาศว่าชนิดหนึ่งเป็นของชั่วคราวแล้วพยายามใช้ราวกับมันอยู่ได้นาน ก็จะเกิดข้อผิดพลาด
      เหมือนกับการที่ฟังก์ชันซึ่งประกาศว่าจะคืน struct Foo แต่กลับคืน Bar แล้วเกิดข้อผิดพลาด เหตุผลที่คอมไพเลอร์ “รู้ดีกว่า” ก็แค่เพราะคุณเขียนบั๊ก
      borrowing เองก็คอมไพล์เป็นการใช้ pointer โดยตรงโดยไม่มี garbage collection และใน struct กับฟังก์ชันแบบ C ABI ก็รับประกันว่าเหมือนกับ C pointer ตามตัวอักษร ถ้าคิดว่าตัวเองรู้ดีกว่าคอมไพเลอร์ ก็สามารถใช้ unsafe เพื่อเลี่ยง lifetime ได้
    • มี crate ที่ให้ parallel iterator อยู่แล้ว แค่เปลี่ยนชื่อการเรียก iter() ก็พอ
      แต่ไม่เห็นด้วยว่ามันควรเป็นโดยปริยาย
    • คำพูดว่า “ควบคุมสิ่งที่คอมไพเลอร์ทำไม่ได้” ออกจะเกินจริงไปหน่อย
      ในบางแง่ Rust ให้การควบคุมแก่โปรแกรมเมอร์มากกว่า C ด้วยซ้ำ เช่น Rust รองรับ inline assembly เป็นมาตรฐาน แต่ inline assembly ของ C ต้องพึ่งส่วนขยายเฉพาะผู้จำหน่าย
      เพียงแต่ค่าเริ่มต้นที่สะดวกนั้นต่างกันมาก ใน Rust การ cast ชนิดแบบไม่ปลอดภัยต้องใช้ขั้นตอนและความระมัดระวังมาก และต้องทำตามกฎมากกว่า C
      โดยเฉพาะ reference ของ Rust โดยพื้นฐานแล้วแทบทั้งหมดทำงานเหมือน restrict และเวลาทำ unsafe cast จาก raw pointer ไปเป็น safe reference จะทำให้พังได้ง่ายมาก ดังนั้นถ้ามีทางเลือก ก็จะเกิดแรงจูงใจอย่างแรงที่จะไม่เขียนโค้ดแบบนั้น
    • ในฐานะคนที่ใช้ unsafe Rust เยอะ รวมถึงสิ่งอย่าง tagged pointer ผมไม่เห็นด้วยอย่างยิ่ง
      กลับกัน ผมอยากได้การควบคุมที่ชัดเจนยิ่งขึ้น และอยากลดภาระนั้นด้วย type system ที่แสดงออกได้มากขึ้น ถ้าให้ดีที่สุด type system ของ Rust ควรกลายเป็นเหมือน Prolog สายพันธุ์หนึ่ง
    • “Bash แบบฉบับ Rust” ก็คงเป็น Bash ที่มี type โดยเฉพาะ floating point มีกรณียกเว้นน้อย มีฟังก์ชันที่รับพารามิเตอร์ชัดเจน และมี flag บรรทัดคำสั่งที่เรียบง่าย
  • การเรียนรู้ว่าเมื่อใดควรลงทุนกับ ข้อจำกัดของ type และเมื่อใดไม่ควร เป็นบทเรียนสำคัญ
    นี่ไม่ใช่ปัญหาเฉพาะของ Rust แต่วิธีแสดงออกอาจต่างกันเล็กน้อย
    ผมเคยรับมือกับ C++ ที่ type หนักเกินไป และ Java ที่ abstract และ type มากเกินไป ทั้งสองมีปัญหา refactoring แบบเดียวกัน
    ในทางกลับกัน ผมก็เห็น Go ที่ขาด type และเอกสารประกอบไม่พอมาเยอะ ซึ่งค่าบางอย่างกระจัดกระจายไปทั่วจนกลายเป็นกับระเบิดตอน runtime และทำให้ refactoring ยากอย่างร้ายแรงได้จริง ๆ
    ความรู้สึกว่าเดินหน้าได้เร็วจะมาเร็วกว่าในช่วงแรก แต่ส่วนใหญ่มักลงเอยด้วยการส่งบั๊กไปให้ผู้ใช้
    ไม่มีคำตอบวิเศษสำหรับ trade-off นี้ Rust เป็นภาษาที่ให้ตัวเลือกบนแกนนี้ค่อนข้างกว้าง

  • คำพูดที่ว่า “Rust ตะโกนใส่คุณทั้งวัน ทุกวัน กับสิ่งที่ในชีวิตเดิมคุณเคยมองว่าเป็นเรื่องปกติอย่างสิ้นเชิง” นั้น คอมไพเลอร์ C ที่ดีก็ทำได้คล้ายกันถ้าเปิดแฟล็กทั้งหมด
    ผมชอบภาษาและคอมไพเลอร์ที่ให้เลือกปิดเสียงตะโกนได้ และยอมให้ตั้งใจเขียนโค้ดแย่ ๆ ได้ โค้ดแย่ ๆ ที่ทำงานได้และเขียนได้เร็ว มักดีกว่าโค้ดที่สมบูรณ์แบบแต่ใช้เวลาชั่วนิรันดร์
    สร้าง proof of concept ที่แย่แต่ทำงานได้ก่อน แล้วค่อยแก้ให้แย่น้อยลงก็ได้
    ประเด็นที่ว่า “ดึงผู้ใช้ใหม่เข้ามาได้ดี แต่ไลบรารีหรือเครื่องมือไม่ได้ดีขึ้นอย่างมาก มีแต่ fork ใช้ครั้งเดียวสำหรับจัดการ use case เฉพาะ” นั้นไม่เกี่ยวกับอายุ
    การดึงดูดนักพัฒนาหลักเป็นเรื่องยาก และต้องใช้ความพยายามมากเพื่อทำให้น่าสนใจ อีกทั้งธรรมเนียมทางวัฒนธรรมถูกกำหนดโดยผู้ใช้กลุ่มแรก ๆ และการไม่มีธรรมเนียมก็ส่งผลเสียได้พอ ๆ กับธรรมเนียมที่แย่
    ดู Python เป็นตัวอย่าง การเข้าหาสภาพแวดล้อมสำหรับพัฒนาและรันแบบหลวม ๆ ทำให้มีวิธีพัฒนาหรือรันโปรแกรม Python แข่งขันกันราว 50 แบบ
    PyPI ซึ่งเป็นที่เก็บแพ็กเกจที่ใช้กันมากที่สุดเคยเละเทะมาหลายปี มีคนไม่มากที่ต่อยอดจากแพ็กเกจเดิม ชื่อแพ็กเกจดูเหมือนสุ่มจากตัวสร้างคำแบบสุ่ม ระบบนิเวศเต็มไปด้วยมัลแวร์ และแม้แต่การค้นหาแพ็กเกจจาก command line ก็ทำไม่ได้
    นี่ไม่ใช่ความผิดของภาษา แต่เป็นความผิดของชุมชนและทีมหลักที่เหมือนยืนดูเฉย ๆ วัฒนธรรมสำคัญกว่าเทคโนโลยีที่อยู่แกนกลางของมัน
    ไม่ได้ตั้งใจจะเจาะจงโจมตี Python เพียงอย่างเดียว แค่รู้ปัญหาของมันดีกว่าเท่านั้น C มีมาแล้วครึ่งศตวรรษ แต่ชุมชนของมันก็ยังจัดระเบียบวิธีแก้ปัญหาที่ภาษาสมัยใหม่กว่าวางไว้ได้ไม่ถึงครึ่งด้วยซ้ำ

    • Rust ก็รองรับเรื่องนั้น แค่ทำเครื่องหมายทั้งหมดเป็น unsafe ก็พอ
    • คงไม่ใช่ว่า Linux กับ Windows เริ่มพอร์ตคอมโพเนนต์ไปเป็น Rust เพราะ “ไม่มีไลบรารีที่ดีขึ้น” หรอก
    • ถ้าจะพูดให้ชัด ๆ มันคล้าย TypeScript มาก
      คือสถานการณ์ที่คอมไพเลอร์ตะโกนให้แก้ แต่คุณแค่อยากลองไอเดียก่อน ก่อนจะขัดเกลาให้สมบูรณ์แบบ