Rust คุ้มค่าขนาดนั้นหรือไม่?
(medium.com/@jsoverson)- หลังจากย้ายจาก 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
- ตัวอย่าง: manual_is_ascii_check
- 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 มากกว่าโค้ดจริง
- ตัวอย่าง: rxRust observable.rs
- ต้องกำหนด generic สำหรับทุก
implจึงยุ่งยากตั้งแต่ตอนเขียนครั้งแรก และเวลา refactor การเปลี่ยนแปลงเล็ก ๆ อาจลามเป็นการแก้ไขต่อเนื่องจำนวนมาก- ตัวอย่าง: Wasmtime Cranelift iter.rs
- เมื่อต้องทำซ้ำ 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 ความคิดเห็น
ความคิดเห็นบน Hacker News
ใช้ Rust มามาก แต่ผ่านไปหลายปีก็ยังรู้สึกว่า ประสิทธิภาพในการทำงานต่ำ
ช่วงนี้ใช้ Zig เยอะ และรู้สึกว่าทำงานได้มีประสิทธิภาพกว่าราว 10 เท่า เพราะโฟกัสกับการเขียนโค้ดที่อยากเขียนได้เลย ไม่ต้องคอยคิดว่าจะใช้เครื่องมือหรือไลบรารีอะไร
รู้ว่า Rust ให้ ความปลอดภัยของหน่วยความจำ และสิ่งนั้นสำคัญ แต่การใช้งานแย่มาก ทุกครั้งที่ใช้ Rust จะรู้สึกถูกจำกัด และต้องคอยหาไลบรารีหรือค้นหาวิธีทำงานอยู่เสมอ จนไม่สามารถแค่ “พิมพ์โค้ด” ได้
ระบบ type ก็อาจขยายจนเหมือนควบคุมไม่อยู่ และหลายครั้งก็ยากจะรู้ว่าใน struct หนึ่ง ๆ เรียกใช้เมธอดอะไรได้จริง Rust เป็นเครื่องมือที่ยอดเยี่ยมและแก้ปัญหาได้มากมาย แต่ไม่คิดว่าเป็นภาษาอเนกประสงค์ที่ดี
จากมุมของคนที่ใช้ Python มาเกือบ 20 ปี ตอนนี้ผมทำงานด้วย Rust ได้เร็วพอ ๆ กับ Python แล้ว
สำหรับผม Rust เหมือนพลาดจุดสมดุลที่เหมาะสมไปโดยสิ้นเชิง จะเขียนแอปพลิเคชันระดับสูงก็จุกจิกกับรายละเอียดระดับต่ำมากเกินไป จะเขียน embedded หรือระบบปฏิบัติการก็ซับซ้อนเกินไป
ถ้าเป็นกรณีแรกคงเลือก C++, Java, Haskell, OCaml หรือแม้แต่ Go ผสม C นิดหน่อย ส่วนกรณีหลัง C ที่ใช้เหมือน macro assembly เหมาะกว่ามาก
ยังรู้สึกอยู่ว่าแนวคิดเดิมของ Graydon Hoare คือภาษาทางสาย OCaml/SML ที่มี linear type, garbage collection, stack allocation, green thread และ CPS น่าจะกลายเป็นภาษาที่ดีกว่ามาก
ใน 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 แล้ว และยังชี้ให้เห็นว่ามีนโยบายห้าม automation ด้วย แต่พวกเขาตอบว่ายังไม่ใช่หลักฐานเพียงพอว่าเป็นการจองชื่อ
ปัญหาของ crates.io คือแม้จะมีนโยบายชัดเจนก็ไม่บังคับใช้ ดังนั้นชื่อ crate ที่สั้นและจำง่ายจึงถูกยึดไปหมดแล้ว และไม่มีทางเอากลับมาได้
NPM, PyPI, RubyGems, Hex ของ Elixir, Cabal ของ Haskell ฯลฯ ช่วงราวปี 2014–2015 ที่ Rust ออกมา แทบนึกตัวอย่าง package manager ที่ไม่ใช่ Java และไม่ได้ใช้ namespace ระดับ global เดียวไม่ออก
บางแห่งพยายามแก้เรื่องนี้ภายหลัง แต่ตอนนั้น package manager ก็ทำงานกันแบบนั้น
ระบบจัดการ dependency ที่แย่กว่าของภาษาอื่น ๆ ที่ออกมาภายหลังแทบไม่ได้เรียนรู้อะไรจากตัวอย่างก่อนหน้าเลย
ข้อดีคือไม่จำเป็นต้องมีฐานข้อมูลแพ็กเกจระดับ global ในระดับภาษาอีกต่อไป ถ้าเอาแพ็กเกจไปไว้ที่ example.com/your-thing ก็เหมือน release ได้ทันที
แน่นอนว่าถ้าต้องการก็สามารถจัด cache และ search engine แยกต่างหากได้
เหมือนกับว่า 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ก็จะเขียนทับการตั้งค่านี้lib.rsหรือmain.rsเรียบง่ายดีกำลังเรียน Rust เพราะเห็นชัดว่ามันจะมีความสำคัญในงานอาชีพ อยากชอบมันจริง ๆ และก็เห็นข้อดีอยู่ แต่จนถึงตอนนี้มันเข้าข่ายเป็นหนึ่งในภาษาที่ ทำให้รู้สึกไม่สบายใจที่สุด เท่าที่เคยใช้มา
ผมหวังมาตลอดว่าพอชำนาญขึ้นแล้วความรู้สึกไม่ชอบจะหายไป แต่ยิ่งไต่เส้นโค้งการเรียนรู้ขึ้นไป ก็ไม่ได้รู้สึกผูกพันกับมันมากนัก
ไม่เป็นไรหรอก นี่คงไม่ใช่ภาษาเดียวที่ผมใช้ได้คล่องแต่ไม่ชอบ เพียงแต่มีคนจำนวนมากบอกว่ารัก Rust ผมเลยคิดว่าตัวเองก็น่าจะสนุกกับมันได้
Rust compiler เองก็ดูเหมือนจะดีขึ้นในการยอมรับกรณีที่ถูกต้องในวงกว้างมากขึ้น
หัวใจของการเข้าใจ borrow checker คือการเข้าใจ memory model ที่อยู่เบื้องหลัง memory model ของ Rust เหมือนกับ C ยกเว้นส่วนขยายเพื่อรองรับ abstraction อย่าง generics
กฎของ borrow checker ตอนแรกอาจดูเหมือนกำหนดขึ้นตามอำเภอใจ แต่จริง ๆ แล้วเชื่อมโยงอย่างลึกซึ้งกับ memory model นี้ คุณค่าที่แท้จริงคือเวลาที่เราไปติด borrow checker โดยไม่ตั้งใจ เพราะนั่นคือบั๊กที่เกิดจากความเผลอเรอ
สิ่งที่น่ากลัวคือในภาษาอย่าง C หรือ C++ โค้ดแบบนั้นอาจถูกยอมรับแล้วปล่อยให้เดินหน้าต่อไป
ระบบชนิดข้อมูลที่เข้มงวดและ borrow checker ของ Rust จะค่อย ๆ ผลักให้เราออกแบบโครงสร้างโค้ดอย่างถูกต้อง และผมมั่นใจว่ามันช่วยปรับปรุงการออกแบบโค้ดของผมในทุกภาษาที่ใช้
นี่เป็นฟีเจอร์ด้าน usability พื้นฐานมากสำหรับโปรแกรมเมอร์ที่ภาษาได้รับความนิยมส่วนใหญ่มีร่วมกัน แม้แต่ C++ ก็มี default arguments มานานแล้ว
ผมเป็นอดีตโปรแกรมเมอร์ C/C++ ที่เรียกใช้ pthread แทบทุกสัปดาห์ตลอด 10 ปี ตอนนี้ใช้ async Rust ไปทุกที่
ไม่เข้าใจว่าทำไม async ถึงถูกเกลียดขนาดนั้น ในความคิดผม ทุกคนควรใช้ async กับทุกอย่าง รวมถึงงาน “ง่าย ๆ” ที่ดูเหมือน single-threaded ด้วย
ถ้า use case ของผู้เขียนเป็น Wasm ก็คงได้มุมมองอีกแบบแน่นอน
งานที่ใช้บัฟเฟอร์ขนาดใหญ่หรือ reuse เพื่อหลีกเลี่ยงต้นทุนการ allocate ก็มักได้ประโยชน์จาก thread pool แบบเก่า ๆ อยู่เหมือนกัน bump allocator หรือ sharded allocator ช่วยได้ในระดับหนึ่ง แต่ถ้าเป็นลูปแน่น ๆ ที่ vectorize ได้และติด CPU-bound แล้ว thread pool มักทำงานได้ดีกว่า
async เป็นเครื่องมือที่ดี แต่ไม่ได้มีแต่บริบทที่เหมาะที่สุดเสมอไป
Go, C#, TypeScript ไม่มีอุปสรรคแบบนั้น
เช่น ดูเหมือนว่าถ้าไม่มี decorator ที่ import มาจาก Tokio ก็
awaitในฟังก์ชันmainไม่ได้ด้วยซ้ำEngineไม่ใช่Sendแต่ผมพยายามใช้มันใน async closureGPT-4 แนะนำให้สร้าง
tokio Runtimeใน thread แล้วใช้block_on()พรุ่งนี้ว่าจะลองดู นี่เป็นโปรเจกต์ Rust จริงจังโปรเจกต์แรกของผมและก็สงสัยว่าคำว่า “ตอนนี้ใช้ async Rust ไปทุกที่” หมายถึง “ใช้ Tokio ไปทุกที่” หรือเปล่า
การเขียนโปรแกรม Rust ไม่เหมือนความสัมพันธ์แบบถูกทำร้ายจริง ๆ compiler พยายามช่วยเต็มที่ โดยเฉพาะ ข้อความ error ของ rustc อยู่ในระดับดีที่สุดของโลก
สิ่งที่ใกล้เคียงกับความสัมพันธ์แบบถูกทำร้ายมากกว่าคือฝั่งระบบปฏิบัติการ ไม่ใช่ Rust compiler ฮาร์ดแวร์ก็เช่นกัน เพราะต้องรัน assembly ให้ถูกต้อง ถ้ามองแบบนั้นก็อาจเรียกได้ว่าเป็นความสัมพันธ์แบบถูกทำร้าย
ข้อความ error ของ Rust นั้นโดดเด่น ไม่มี compiler ตัวไหนเข้าใกล้ได้เลย
แถมล่าสุดผมใช้ LaTeX แล้วข้อความ error แย่มาก การต้องจ้อง error เพื่อหาว่าอะไรผิดพลาดเป็นฝันร้ายจริง ๆ
Futureไม่ใช่SendและSyncอีกต่อไปในทุกจุดเรียกซ้ำทั้ง console ถูก error ถมเต็มไปหมด และ syntax error จริง ๆ ถูกฝังอยู่ตรงไหนสักแห่งกลาง ๆ นั้น
ข้อตำหนิหลักคือผมยังไม่เข้าใจ lifetime อย่างสมบูรณ์ และ compiler ก็ช่วยไม่ได้ทุกครั้ง พอเข้าใจได้ เพราะ compiler ต้องตัดสินแบบอนุรักษนิยม
ในการทดสอบ C++ มักมีคำพูดว่า “ถ้าคอมไพล์ผ่าน โดยมากก็ถูกแล้ว”
ถ้าคุณคิดว่า Rust จัดการข้อผิดพลาดให้มากจนกรณีทดสอบทั่วไปกลายเป็นไม่มีความหมาย ผมมองว่านั่นเป็นสัญญาณว่าคุณไม่ได้ทดสอบสิ่งที่ถูกต้องในภาษาอื่น
สิ่งที่ควรทดสอบไม่ใช่ปัญหาของตัวภาษาเอง แต่คือ business logic
ถ้าดูโค้ดทดสอบแล้วรู้สึกว่า “ถ้าเป็น JavaScript ก็คงทดสอบ แต่ใน Rust ไม่ต้องก็ได้” ก็ลบทดสอบนั้นทิ้งได้เลย
ไม่มีการแบ่งแยกว่า “business logic เทียบกับปัญหาของภาษา” เพราะภาษาคือฐานที่ business logic ตั้งอยู่
ถ้าไม่ทดสอบวิธีการล้มเหลว ก็ไม่รู้ว่าการทดสอบมีความหมายอะไร
ต่างจากภาษาส่วนใหญ่ที่คุ้นเคย C++ มี IFNDR และถูกเรียกแบบติดตลกว่าเป็น false positive ต่อคำถาม “นี่เป็นโปรแกรม C++ หรือเปล่า?”
คอมไพเลอร์ C++ ที่เป็นไปตามมาตรฐานถูกห้ามไม่ให้บอกในบางกรณีที่น่าสงสัยว่าโค้ดที่เขียนมานั้นไม่สมเหตุสมผล และต้องเดินหน้าต่อไปเพื่อพ่นอะไรบางอย่างออกมา
นั่นอาจเป็นไฟล์รันได้ที่ทำงานได้ หรืออาจเป็นไฟล์รันได้ที่ก่อมหันตภัยทุกวันศุกร์ก็ได้ ไม่มีทางรู้
มาตรฐาน ISO ระบุกรณีเหล่านี้ไว้ก็จริง แต่มันคลุมเครือมากจนยากจะจับให้ชัดว่ารวมอะไรบ้าง และผมเดาว่าซอฟต์แวร์ C++ ที่ไม่ trivial ส่วนใหญ่ในปัจจุบันน่าจะเป็น IFNDR จริง ๆ ทางที่ดีกว่าคือปฏิเสธภาษาทั้งหมดไปเลย
Rust ดูเหมือนจะทำลายความคิดที่ว่าโปรแกรมเมอร์ต้องควบคุมและรับรู้ทุกอย่างที่คอมไพเลอร์ทำได้ในที่สุด
อันที่จริงมันไม่เป็นแบบนั้นมาตั้งหลายทศวรรษแล้ว และคอมไพเลอร์ก็แทบจะเหมือนเวทมนตร์ Rust ย้อนกระแสนั้นกลับไปมากด้วย borrowing ทำให้ผู้คนสบายใจกับการที่คอมไพเลอร์รู้ดีกว่าตัวเอง
อยากให้สบายใจกว่านี้อีก ถ้าไม่ได้จำเป็นในเชิงอัลกอริทึม ก็ไม่ควรต้องวนซ้ำ collection จากหน้าไปหลังอย่างชัดเจน งานหลายอย่างควรถูก parallelize โดยปริยาย อยากได้ Bash แบบฉบับ 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 ได้iter()ก็พอแต่ไม่เห็นด้วยว่ามันควรเป็นโดยปริยาย
ในบางแง่ Rust ให้การควบคุมแก่โปรแกรมเมอร์มากกว่า C ด้วยซ้ำ เช่น Rust รองรับ inline assembly เป็นมาตรฐาน แต่ inline assembly ของ C ต้องพึ่งส่วนขยายเฉพาะผู้จำหน่าย
เพียงแต่ค่าเริ่มต้นที่สะดวกนั้นต่างกันมาก ใน Rust การ cast ชนิดแบบไม่ปลอดภัยต้องใช้ขั้นตอนและความระมัดระวังมาก และต้องทำตามกฎมากกว่า C
โดยเฉพาะ reference ของ Rust โดยพื้นฐานแล้วแทบทั้งหมดทำงานเหมือน
restrictและเวลาทำunsafecast จาก raw pointer ไปเป็น safe reference จะทำให้พังได้ง่ายมาก ดังนั้นถ้ามีทางเลือก ก็จะเกิดแรงจูงใจอย่างแรงที่จะไม่เขียนโค้ดแบบนั้นกลับกัน ผมอยากได้การควบคุมที่ชัดเจนยิ่งขึ้น และอยากลดภาระนั้นด้วย type system ที่แสดงออกได้มากขึ้น ถ้าให้ดีที่สุด type system ของ Rust ควรกลายเป็นเหมือน Prolog สายพันธุ์หนึ่ง
การเรียนรู้ว่าเมื่อใดควรลงทุนกับ ข้อจำกัดของ 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 มีมาแล้วครึ่งศตวรรษ แต่ชุมชนของมันก็ยังจัดระเบียบวิธีแก้ปัญหาที่ภาษาสมัยใหม่กว่าวางไว้ได้ไม่ถึงครึ่งด้วยซ้ำ
คือสถานการณ์ที่คอมไพเลอร์ตะโกนให้แก้ แต่คุณแค่อยากลองไอเดียก่อน ก่อนจะขัดเกลาให้สมบูรณ์แบบ