1 คะแนน โดย GN⁺ 2024-09-27 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • Rust มีจุดเด่นทั้ง ความปลอดภัยของหน่วยความจำ ประสิทธิภาพ algebraic types และข้อดีของ Cargo แต่ผู้เขียนเริ่มจากความรู้สึกว่าช่องทาง stable พัฒนาภาษาได้ช้าลง จนให้ความรู้สึกเหมือน “ผลิตภัณฑ์รุ่นแรกที่ยังไม่เสร็จสมบูรณ์”
  • หลังชุมชนขยายใหญ่ขึ้น การตัดสินใจบนฐานฉันทามติ กลายเป็นคอขวด และฟีเจอร์อย่าง coroutine แม้จะถูก implement แล้ว ก็ยังสะสมเป็นฟีเจอร์ที่ใช้งานใน stable Rust ไม่ได้อยู่นาน
  • “seph edition” ที่สมมติขึ้นเป็นแนวคิดที่จะคงระบบนิเวศ Rust และ Cargo เดิมไว้ แต่ยอมรับ breaking change เพื่อทดลองระบบ effect, capability ตอนคอมไพล์, การ borrow ฟิลด์ของ struct, comptime เป็นต้น
  • หากต้องการลดความเสี่ยงด้านซัพพลายเชน ก็ควรผูก capability กับความสามารถอ่อนไหวอย่างการเขียนไฟล์ เครือข่าย FFI และ unsafe และให้ผู้เรียกอนุญาตอย่างชัดเจน
  • ข้อเสนอส่วนใหญ่เข้ากันไม่ได้กับ Rust เดิม และแม้แต่การเพิ่ม capability เพียงอย่างเดียวก็ทำให้เกิดเงื่อนไขความล้มเหลวแบบใหม่ที่ทำลายความเข้ากันได้ของ semver จึงต้องมี edition ใหม่หรือแยกคอมไพเลอร์ออกมา

ทำไม Rust ถึงให้ความรู้สึกเหมือน “ผลิตภัณฑ์รุ่นแรก”

  • ตอนแรก Rust ดูน่าดึงดูดมากจาก algebraic types, ความปลอดภัยของหน่วยความจำโดยไม่เสียประสิทธิภาพ และตัวจัดการแพ็กเกจสมัยใหม่
  • แต่หลังจากใช้งานมาราว 4 ปี ก็เริ่มรู้สึกว่าภาษา “ขาดอะไรไปนิดหน่อยอยู่เสมอ”
  • แก่นของความไม่พอใจคือ ฟีเจอร์ใหม่เข้าสู่ stable Rust น้อยลง และความเร็วในการพัฒนาภาษาก็ช้าลงมาก
  • Rust unstable book มี unstable feature ราว 700 รายการ โดยจำนวนมากเกี่ยวข้องกับการเปลี่ยนแปลงใน standard library
  • Coroutines เป็นฟีเจอร์ที่มี RFC มาแล้ว 7 ปี และมี implementation อยู่ในคอมไพเลอร์แล้ว แต่ผู้ใช้ stable Rust ก็ยังใช้ไม่ได้
  • กระบวนการ Rust RFC ดูเหมือนเป็นที่ที่ไอเดียดี ๆ เข้าไปค้างอยู่นาน
    • มีการยกตัวอย่างการถกเถียงเรื่องการปรับปรุง Mutex ที่มีผู้ร่วมแสดงความเห็น 25 คน ตลอด 2 ปี รวมกว่า 200 คอมเมนต์ แต่ก็ยังไม่ได้ข้อสรุปชัดเจน

แนวคิด “seph edition” ที่สมมติขึ้น

  • ผู้เขียนจินตนาการถึงการ fork คอมไพเลอร์ แต่ปล่อยโค้ด Rust เดิมไว้เหมือนเดิม แล้วเพิ่ม Rust edition แยกต่างหากชื่อ “seph”
  • ใน edition นี้จะยอมรับ breaking change แต่ถ้าคอมไพเลอร์ยังคอมไพล์ mainline Rust ได้ ก็ยังใช้ crate เดิมของ Cargo ต่อได้
  • แกนหลักที่อยากเปลี่ยนมี 5 เรื่องใหญ่
    • function trait และระบบ effect
    • capability ตอนคอมไพล์
    • ออกแบบ Pin, move และ struct borrow ใหม่
    • comptime แบบ Zig
    • การปรับ syntax และ standard library เล็ก ๆ น้อย ๆ

Function trait และระบบ effect

  • Rust มี trait สำหรับ struct อยู่แล้ว แต่ข้อเสนอคือควรใส่ trait/effect ที่หลากหลายขึ้นให้กับฟังก์ชันได้ด้วย
  • คุณสมบัติที่ฟังก์ชันอาจมี ได้แก่
    • panic ได้หรือไม่
    • ใช้ stack ขนาดคงที่หรือไม่
    • รันจนจบหรือมี yield หรือ await
    • ถ้าเป็น coroutine continuation จะมี type อะไร
    • เป็น pure function หรือไม่
    • มีการรัน unsafe code ทางอ้อมหรือไม่
    • รับประกันการสิ้นสุดการทำงานหรือไม่
  • หากเปิดเผยพารามิเตอร์และชนิดคืนค่าของฟังก์ชันเหมือนเป็น associated type ของฟังก์ชัน ก็จะทำให้สามารถอ้างอิง type แบบที่ stable Rust ปัจจุบันทำไม่ได้
  • ตัวอย่างคือโค้ดที่ตั้งชื่อชนิดคืนค่าของฟังก์ชันโดยตรงแบบ some_iter::Output แล้วนำไปใช้ในฟิลด์ของ struct
  • ในกรณีอย่าง Linux kernel ที่ต้องการรับประกันว่าโค้ดบางบล็อกจะไม่มีวัน panic ก็อาจตรวจความเป็นไปได้ของ panic รวมถึงการเรียกซ้ำได้ผ่านการระบุอย่าง #[disallow(Panic)]
  • คอมไพเลอร์จัดการ trait ที่เกี่ยวกับฟังก์ชันอย่าง Fn, FnOnce, FnMut อยู่แล้ว แต่ผู้เขียนมองว่ารูปแบบปัจจุบันยังเรียบง่ายเกินไป
  • มีลิงก์อ้างอิงเพิ่มเติมไปยัง บทความและงานนำเสนอเรื่อง effect system ของ Yoshua Wuyts

ลดความเสี่ยงซัพพลายเชนด้วย capability ตอนคอมไพล์

  • โปรเจ็กต์ Rust ส่วนใหญ่นำเข้า crate จากภายนอกจำนวนมาก และแม้แต่ utility crate เล็ก ๆ ก็สร้าง ความเสี่ยงด้านซัพพลายเชน ได้ผ่านการอัปเดตที่เป็นอันตราย
  • เช่นเดียวกับที่ unsafe ในเรื่องความปลอดภัยของหน่วยความจำเป็นการ opt-in แบบชัดเจน ความสามารถอ่อนไหวอย่างระบบไฟล์ เครือข่าย FFI และ raw pointer ก็ควรถูกอนุญาตอย่างชัดเจนเช่นกัน
  • ข้อเสนอคือให้ใส่ marker tag กับฟังก์ชันที่อ่อนไหวด้านความปลอดภัยใน standard library
    • ตัวอย่าง: std::fs::write(path, contents) สามารถเขียนไฟล์ไปยัง path ใดก็ได้ จึงอาจมีแท็กอย่าง #[cap(fs_write)]
    • คอมไพเลอร์จะทำ taint ให้ทั้ง call tree ที่เรียกฟังก์ชันนี้โดยอัตโนมัติ
  • หาก crate ภายนอกต้องการ capability fs_write ผู้เรียกต้องอนุญาตอย่างชัดเจนผ่าน Cargo.toml หรือ annotation ที่จุดเรียก
  • หากไม่อนุญาต คอมไพเลอร์อาจแจ้งว่า foo::do_stuff() มีการเขียนลง local file system แต่ crate foo ไม่ได้รับความเชื่อถือสำหรับ capability นั้น
  • utility crate หลายตัวอย่าง human-size หรือ serde ไม่จำเป็นต้องใช้ capability พิเศษ ดังนั้นแม้ผู้เขียนจะใส่โค้ดอันตรายเพิ่มเข้ามา ก็ยังถูกขัดขวางไม่ให้เขียนไฟล์หรือใช้งานเครือข่ายได้ตั้งแต่ขั้นคอมไพล์
  • อีกทางเลือกหนึ่งคือเปลี่ยน API ที่อ่อนไหวให้รับพารามิเตอร์ Capability แยกต่างหาก
    • เช่น std::fs::write อาจต้องรับค่า FsWriteCapability
    • และจำกัดให้สร้างอ็อบเจ็กต์ Capability ได้เฉพาะใน root crate
  • วิธีนี้เพิ่ม boilerplate แต่ยืดหยุ่นกว่า และจำเป็นต้องใช้แนวทางคล้ายกันกับสคริปต์ build.rs และบล็อก unsafe ด้วย
  • แม้ในสถานการณ์ที่ crates.io ถูกเจาะและมีการใส่โค้ด cryptolocker ลงใน serde แนวทาง capability ก็จะทำให้เกิดข้อผิดพลาดตอนคอมไพล์ก่อนที่โค้ดนั้นจะรันบนเครื่องนักพัฒนาหลายเครื่องหรือถูกรวมเข้าไปในไบนารี

ออกแบบ Pin, move และ struct borrow ใหม่

  • ผู้เขียนมองว่า Pin เป็นแฮ็กที่ซับซ้อนเพื่ออ้อมช่องโหว่ของ borrow checker และเป็นเพียง band-aid ที่เกิดจากการพยายามรักษาความเข้ากันได้ย้อนหลัง
  • สิ่งที่ต้องการจริง ๆ น่าจะใกล้เคียงกับ marker trait Move สำหรับระบุ type ที่เคลื่อนย้ายได้
  • ปัจจุบัน Rust ไม่มี trait Pin แต่มี Unpin และ !Unpin ซึ่งรูปแบบปฏิเสธซ้อนนี้ทำให้แนวคิดเข้าใจยากขึ้น
  • มีคำวิจารณ์ว่า Pin ใช้ได้เฉพาะกับ reference type จึงทำให้เกิดโค้ดที่ต้องห่อด้วย Box อยู่หลายแห่ง
    • มีการยกตัวอย่างลิงก์ไปยัง Tokio stream wrapper, ouroboros, async-trait, self_cell และ helper library อื่น ๆ
  • ฟังก์ชันที่รับค่าซึ่งถูก pin ไว้ เช่น Future::poll(self: Pin<&mut Self>, ..) ก็ซับซ้อนขึ้น และยังต้องพึ่ง crate แยกสำหรับจัดการ projection
  • ภายในฟังก์ชัน borrow checker จัดการตัวแปรด้วยสถานะอย่าง “owned”, “borrowed”, “mutably borrowed” แต่สถานะเหล่านี้ไม่ได้แสดงให้โปรแกรมเมอร์เห็นโดยตรง
  • async fn ทำงานโดยให้คอมไพเลอร์สร้าง hidden struct เพื่อเก็บสถานะระหว่างการหยุดรอ และในกระบวนการนี้อาจเกิดรูปแบบที่ฟิลด์หนึ่ง borrow อีกฟิลด์หนึ่ง
  • Rust ไม่มี syntax สำหรับแสดงว่าฟิลด์ของ struct อยู่ในสถานะ borrowed และไม่สามารถระบุ lifetime ระหว่างฟิลด์ได้โดยตรง
  • ทิศทางที่เสนอคือขยาย borrow checker เพื่อให้เขียน struct field borrow ได้โดยตรง
    • เช่น syntax แบบ “local borrow” อย่าง y: &'Self::x Vec<usize>
    • คอมไพเลอร์จะรู้ว่า x ถูก borrow อยู่ และใช้ข้อจำกัดเดียวกับตัวแปร borrowed ภายในฟังก์ชัน
  • syntax แบบนี้ยังใช้กับ self-referential struct หรือกรณีอย่าง AST ที่มี source: String และ ast_nodes: Vec<&'Self::source str> ได้ด้วย
  • struct ที่มี borrowed field จะไม่สามารถถูกย้ายได้ จึงไม่ implement Move และยังมีการพิจารณา trait Mover สำหรับการย้ายตัวเองอย่างปลอดภัยด้วย

comptime แบบ Zig และปัญหา macro ของ Rust

  • ผู้เขียนมองว่าคอมไพเลอร์ Rust ในทางปฏิบัติต้องจัดการหลายภาษาไปพร้อมกัน ทั้ง Rust, ภาษา macro ของ Rust และ proc macro
  • ประเด็นสำคัญคือ Rust เองดีอยู่แล้ว แต่ ภาษา macro นั้นไม่ดี
  • comptime ของ Zig คือแนวทางที่คอมไพเลอร์มีอินเทอร์พรีเตอร์ขนาดเล็กสำหรับรันบางส่วนของโค้ดในช่วงคอมไพล์
  • สามารถระบุให้ฟังก์ชัน พารามิเตอร์ if และ loop เป็นโค้ดช่วงคอมไพล์ได้ และโค้ดที่ไม่ใช่ comptime จะถูก emit เป็นโปรแกรมจริง
  • กรณีศึกษา Zig std print แสดงตัวอย่างการรับ format string เป็นพารามิเตอร์ comptime แล้ว parse ด้วย comptime loop เพื่อสร้างโค้ดสำหรับพิมพ์ผลลัพธ์
  • การทำงานของ println!() ใน Rust อาศัยการเรียกฟังก์ชันภายในอย่าง format_args_nl ซึ่งผู้เขียนคาดว่าน่าจะถูก hardcode ไว้ในคอมไพเลอร์
  • จึงตามมาด้วยคำวิจารณ์ว่าแม้แต่ผู้เขียน Rust compiler เองก็ดูเหมือนไม่อยากใช้ภาษา macro ของ Rust

ข้อเสนอปรับเล็ก ๆ ใน syntax และ standard library

  • มีข้อเสนอว่าอยากแก้ปัญหาที่ Range<T> implement Copy เมื่อ T: Copy
  • ปัญหา derive ที่มี associated type ก็ถูกมองว่าเป็นอีกจุดที่ควรแก้
  • มีข้อเสนอว่า expression if let ควรรองรับ logical AND
    • รูปแบบที่ต้องการ: if let Some(x) = some_var && some_expr { }
    • วิธีเลี่ยงในปัจจุบันคือ match tuple แบบ (some_var, check_foo()) แต่ check_foo() จะยังทำงานแม้ some_var เป็น None ซึ่งต่างจากพฤติกรรม short-circuit ของ if ปกติ
    • ตัวอย่าง: Playground
  • เรื่อง ergonomics ของ raw pointer ก็ถูกยกเป็นเป้าหมายการปรับปรุง
    • กับ reference เราเขียน myref.x ได้ แต่กับ pointer ต้องเขียน (*myptr).x หรือ (*(*myptr).p).y
    • ผู้เขียนเห็นว่า unsafe code ควรอ่านและเขียนได้ง่ายที่สุดเท่าที่จะทำได้
  • ยังมีข้อเสนอว่า built-in collection type ควรรับ Allocator อย่างชัดเจนใน constructor แทนการพึ่ง global allocator
  • ส่วน async ก็ยังต้องปรับอีกมาก แต่ผู้เขียนบอกว่าเรื่องนี้ใหญ่พอจะต้องแยกไปเขียนอีกบทความหนึ่ง

ความเข้ากันได้และความเป็นไปได้ในการทำจริง

  • ข้อเสนอส่วนใหญ่เข้ากันไม่ได้กับ Rust เดิม
  • แม้แต่การเพิ่ม security capability ก็ทำให้ crate เกิดเงื่อนไขความล้มเหลวรูปแบบใหม่ที่ทำลายความเข้ากันได้ของ semver จึงต้องใช้ Rust edition ใหม่
  • ผู้เขียนบอกว่าถ้าเป็นเมื่อหลายปีก่อนอาจจะเขียน RFC ไปแล้ว แต่ตอนนี้ไม่อยากผ่านกระบวนการคอมเมนต์ RFC บน GitHub อันยาวนาน เพียงเพื่อให้มันกลายเป็นอีกหนึ่งไอเดียที่ไม่ถูกทำจริง
  • สุดท้ายจึงย้อนกลับมาที่จินตนาการเรื่องการ fork คอมไพเลอร์เอง แต่ก็ยังติดข้อจำกัดตามความเป็นจริงว่ามีโปรเจ็กต์อื่นที่ต้องทำมากเกินไป

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

 
GN⁺ 2024-09-27
ความคิดเห็นจาก Hacker News
  • ตรงข้ามกับคำกล่าวที่ว่ากระบวนการ RFC ของ Rust เป็นสุสานของไอเดียดี ๆ ผมมองว่าการที่ทีมหลักของ Rust ทำให้การเพิ่ม ฟีเจอร์ใหม่ เข้าไปในภาษาโปรแกรมเป็นเรื่องยากนั้นเป็นทิศทางที่ถูกต้องแล้ว
    ต้องป้องกันไม่ให้พื้นผิวของภาษาบวมเทอะทะ ขาดความสอดคล้อง และคาดเดาไม่ได้ ตอนแรกผมก็ชอบ Swift แต่พอมีสิ่งต่าง ๆ เพิ่มขึ้นเรื่อย ๆ เช่น ชื่อฟังก์ชันที่ซ้ำซ้อนอย่าง isMultiple(of:), กฎการ parse วงเล็บปีกกาสำหรับ SwiftUI, กฎเรื่องชนิดแบบอ้างอิง/ค่าและ mutability, การเขียนย่ออาร์กิวเมนต์ของ closure สุดท้ายก็ยอมแพ้ไป ไอเดียดี ๆ มีอยู่ทั่วไป เลยอยากให้ Rust คงความ เพรียวบาง ไว้ให้มากที่สุด

    • ผมเป็นผู้เขียนบทความต้นฉบับ เวลาใช้ Rust มักมี ช่องโหว่ ที่ฟีเจอร์ X กับฟีเจอร์ Y รองรับแยกกัน แต่ ใช้ร่วมกันไม่ได้ อยู่บ่อย ๆ
      ตัวอย่างเช่น ฟังก์ชันที่คืนค่า impl Trait ใช้ได้ และ struct ก็มีฟิลด์ใด ๆ ได้ แต่เอาค่าที่คืนมาด้วย impl Trait ไปใส่ในฟิลด์ของ struct ไม่ได้ เพราะตั้งชื่อ type ให้มันไม่ได้ if a && b ทำได้ และ if let Some(x) = x ก็ทำได้ แต่ if let Some(x) = x && b ทำไม่ได้ เรื่องแบบนี้อยากให้แก้ ถ้าวัดตามจำนวนบรรทัดโค้ดของคอมไพเลอร์ Rust คงใหญ่ขึ้น แต่ถ้าวัดตามความซับซ้อนในการเรียนรู้และใช้งาน ช่องโหว่ของฟีเจอร์แบบนี้กลับทำให้ภาษาซับซ้อนยิ่งกว่าเดิม Pin ไม่ใช่งานใหญ่ในการ implement ไว้ใน standard library แต่ก็ไม่ใช่ฟีเจอร์ที่เพรียวบางเลย และเพิ่มภาระทางความเข้าใจมหาศาล ผมคิดว่าการมี borrow checker ที่ซับซ้อนแต่ได้โค้ด Rust ที่อ่านง่าย ดีกว่าคอมไพเลอร์ที่เรียบง่ายแต่ภาษาใช้งานยาก
    • การเขียน RFC ง่ายกว่าการ implement และคนที่เขียน RFC ได้ก็มีมากกว่า ดังนั้นไม่ว่าระดับความนิยมจะเป็นอย่างไร คิว RFC ก็ย่อมดูเหมือนสุสานของไอเดียดี ๆ อยู่ดี
      ต่อให้ไอเดียที่รออยู่ถูกยอมรับ 100% และสุดท้ายถูกนำไปทำจริง มันก็ยังดูเป็นแบบนั้นเพราะความต่างระหว่างอัตราที่ไอเดียเข้ามากับอัตราที่งานออกไป ถ้าอยากไปให้ไกลกว่าคนเขียน RFC ก็ควรไม่ใช่แค่เขียน RFC แต่ทำ implementation ที่พร้อมใช้ใน production พร้อมเอกสารและเทสต์ ให้สามารถ merge เข้าต้นไม้โค้ดได้อย่างสะอาด
    • โดยรวมเห็นด้วย แต่ associated constant แบบ generic ของ trait อยู่ในสถานะย้อนแย้งคือ stable แล้ว แต่ใช้เหมือน constant จริง ๆ ไม่ได้
      ใช้เป็น const generic ของ type อื่นก็ไม่ได้ ใช้เป็นความยาว array ก็ไม่ได้ ถ้าแค่ต้องการค่า ก็แค่ define ฟังก์ชันให้คืนค่ามาก็พอ ดังนั้นตอนนี้แทบไม่มีประโยชน์ ถ้ารองรับได้ถูกต้อง ก็จะกำจัด crate เสริมอย่าง generic_array, typenum ในไลบรารีเข้ารหัสได้ ถึงอย่างนั้นก็ยังเห็นด้วยว่าทีม Rust ควรระมัดระวังในการเพิ่มฟีเจอร์
    • Rust ซึ่งเป็นภาษาโปรแกรมที่ค่อนข้าง mainstream อยู่แล้ว ตอนนี้อยู่ในสภาพเหมือน เนื้อวากิว จึงไม่ควรเพิ่มไขมันเข้าไปอีกจนกินไม่ได้
    • ไอเดียที่ดี ตามนิยามแล้วคือสิ่งที่ หายากและมีค่า
  • สถานการณ์เรื่อง dependency ค่อนข้างรุนแรง แต่ดูเหมือนไม่ค่อยมีใครอยากยอมรับ ตัวอย่างที่เพิ่งเห็นคือ crate cargo-watch
    แก่นของมันคือแอปง่ายๆ ที่คอยเฝ้าดูการเปลี่ยนแปลงของไฟล์แล้วรันคอมไพเลอร์ใหม่ และตัว implementation ก็ไม่ถึง 1,000 บรรทัด แต่พอ vendor dependency เข้ามา โค้ด Rust กลับพองไปเกือบ 4 ล้านบรรทัด กระจายอยู่ในไฟล์กว่า 8,000 ไฟล์ สำหรับตัวเฝ้าดูไฟล์ธรรมดาๆ แล้วถือว่าเกินไป
    https://crates.io/crates/cargo-watch

    • ถ้าทำให้สร้าง dependency แบบ transitive ได้ง่าย และมีวัฒนธรรมว่า “ถ้ามีไลบรารีอยู่แล้วก็ต้องใช้” ผลก็ย่อมออกมาแบบนั้นอย่างเลี่ยงไม่ได้
      C/C++ แทบเป็นกรณีเดียวในบรรดาภาษาที่ใช้กันกว้างขวางที่ package manager แบบ npm ยังไม่เป็นกระแสหลัก ดังนั้นไลบรารีส่วนใหญ่จึงมัก self-contained หรือมี dependency น้อยและเป็นทางเลือก efsw คือ file system watcher ภาษา C++ ขนาด 7,000 บรรทัดที่ไม่มี dependency ไลบรารีแบบ single-header ในสาย game programming อย่าง stb_*, cgltf และ Dear ImGui เป็นประสบการณ์ที่สบายที่สุดเท่าที่เคยใช้มา ผมคิดว่า package manager รุ่นใหม่อาจได้ประโยชน์โดยรวมจากการ ห้าม transitive dependency ส่วน dependency ที่เป็นไลบรารีขนาดใหญ่ก็ให้ผู้ใช้ติดตั้งเองได้ หรือจัดให้ผ่าน callback หรือทำให้เป็นฟีเจอร์มาตรฐาน
      https://github.com/SpartanJ/efsw
      https://github.com/nothings/stb
      https://github.com/jkuhlmann/cgltf
      https://github.com/ocornut/imgui
    • ทุกวันนี้ ป่าดง dependency เป็นวิธีปกติในการทำงานให้เสร็จ สิ่งที่ runtime ทำได้ดีที่สุดคือยอมรับมัน ทำให้เร็วและปลอดภัยที่สุดเท่าที่ทำได้ และขยาย standard library ให้กว้างขึ้นเพื่อรองรับโปรเจกต์ที่ใช้ dependency น้อยที่สุดด้วย
      ผมมองว่า file watcher โดยเฉพาะเมื่อรองรับหลายแพลตฟอร์ม ไม่ได้เรียบง่ายเลย
    • เป็นปรากฏการณ์ที่เป็นธรรมชาติและไม่ได้ชวนกลัวมากนัก โค้ดทุกตัวตั้งอยู่บน เทือกเขา dependency ที่มีฟังก์ชันมากกว่าที่ตัวเองใช้จริงมากมาย
      ส่วนหนึ่งของ cargo watch ดึงไลบรารี wrapper ของ Win32 API เข้ามา ซึ่งเป็น binding ที่สร้างอัตโนมัติสำหรับการเรียก Win32 จึงหลีกเลี่ยงไม่ได้ที่จะมีขนาดใหญ่ standard library ของภาษาส่วนใหญ่ก็มีเป็นล้านบรรทัด และแอปก็ใช้แค่บางส่วนเท่านั้น Boost ของ C++ ก็ใหญ่โตเหมือนสัตว์ประหลาด แต่ developer ก็ดึงมาใช้แค่ extension บางส่วน ที่จริง dependency ขนาดใหญ่จำนวนน้อยกว่าซึ่งมีหลายคนดูแลและพึ่งพา ยังดีกว่านรกแบบ npm ที่แพ็กเกจอย่าง isOdd หรือ is even เพียงตัวเดียวอาจทำให้ทั้ง ecosystem พังได้ และโดยรวม Rust ค่อนข้างอยู่ฝั่งนั้นมากกว่า
    • ที่ทำงานผมตั้งใจถอด dependency หลายตัวออกแล้วเขียนใหม่อยู่บ่อยๆ แต่ไม่ว่าจะ 1,000 บรรทัดหรือ 4 ล้านบรรทัด ทั้งคู่ก็ดูไม่ใช่การประมาณจำนวนบรรทัดของโค้ดที่เหมาะสมสำหรับโปรเจกต์นี้
      dependency ส่วนใหญ่ของ cargo-watch น่าจะมาจาก requirement โดยตรง 3 ตัวคือ clap, cargo_metadata, watchexec clap ดึงองค์ประกอบ CLI เฉพาะแพลตฟอร์มเข้ามาเยอะ และ cargo_metadata ก็เลี่ยงไม่ได้ที่จะดึงตระกูล serde เข้ามามาก watchexec ยังมีช่องให้ปรับปรุง เพราะมันพึ่งพา command-group ที่องค์กรเดียวกันดูแล และตัวนี้บังคับต้องใช้ Tokio ปัญหาที่ใหญ่กว่าคือเราแก้ dependency ที่มากเกินไป ของ crate อื่นได้ไม่ง่าย ถ้ามี crate ที่ผูกอยู่กับ watchexec รุ่นเก่า บางครั้งอาจปรับ Cargo.lock ให้เข้ากันได้ แต่โดยทั่วไปทำยากและต้องอ้อมด้วย [patch] คงดีถ้ามีวิธีกำหนด “stand-in” เพื่อแทนที่ crate เวอร์ชันใดเวอร์ชันหนึ่งได้ง่ายๆ แต่สำหรับ package manager ที่มีอยู่แล้ว น่าจะเป็นโจทย์วิจัยใหญ่
    • จำนวนบรรทัดส่วนใหญ่น่าจะมาจาก Windows API crate ที่สร้างขึ้นอัตโนมัติ พวกนี้ขึ้นชื่อว่าใหญ่โตมาก
  • Rust ไม่ใช่ภาษาหน้าใหม่ที่น่าตื่นเต้นอีกต่อไปแล้ว แต่กำลังเข้าสู่ช่วงมุ่งสู่ การนำไปใช้ในวงกว้าง การที่การพัฒนาฟีเจอร์ช้าลงเป็นเรื่องธรรมชาติและดีต่อสุขภาพของภาษา
    ตอนนี้ความผิดพลาดด้านการออกแบบเป็นอันตรายมากกว่าความเร็วที่ต่ำมาก เหตุผลที่ Rust น่าสนใจไม่ใช่เพราะฟีเจอร์เท่ๆ แต่เพราะมันเป็นภาษากลุ่มใหม่ที่ memory-safe, ไม่มี garbage collection และพร้อมใช้ในโปรดักชัน การถูกนำไปใช้จริงในจุดสำคัญน่าสนใจกว่าการทำให้ภาษาดีขึ้นอีก และการที่ผู้คนเชื่อว่ามันถูกบริหารอย่างระมัดระวังก็ทำให้เรื่องนั้นง่ายขึ้น

    • JavaScript ก็อาจถือว่าอยู่ในช่วงชีวิตคล้ายกัน แต่ตลอด 10 ปีที่ผ่านมาได้เพิ่ม ฟีเจอร์ใหม่ที่ยอดเยี่ยม เข้ามาเพียบ
      spread operator, generator function, async, arrow function, leftpad, Date ใหม่ และรายการฟีเจอร์สำคัญๆ ยังไม่จบแค่นั้น JS เก่ากว่า Rust มากและถูกใช้กว้างขวางกว่ามาก อีกทั้งมี implementation สำหรับโปรดักชันหลายตัว จึงต้องทำให้ฟีเจอร์ใหม่ทั้งหมดสอดคล้องกัน เคยมีช่วงชะงักแถวๆ ES5 แต่ความต่างดูเหมือนอยู่ที่คณะกรรมการมาตรฐาน ECMAScript ตั้งสติได้แล้ว
    • Ada/SPARK มีมานานแล้ว จึงสงสัยว่านี่เป็นภาษากลุ่มใหม่จริงหรือไม่ SPARK พาความปลอดภัยไปไกลกว่านั้นอีกด้วยซ้ำ โดยรวมถึง formal verification
  • Rustacean หมกมุ่นกับการเขียนทุกอย่างใหม่ด้วย Rust มาก จนผมนึกจริงๆ ว่าถ้าเป็นบทความเขียน Rust ใหม่ ก็น่าจะเป็น มุกเสียดสีเชิงเมตา

    • เรื่องนี้เกิดขึ้นไปแล้วตั้งแต่ยุคก่อนประวัติศาสตร์ เดิมที Rust เขียนด้วย OCaml แต่สุดท้ายก็ถูกเขียนใหม่ด้วย Rust
    • ผมเป็นผู้เขียนบทความต้นฉบับเอง สิ่งที่ตั้งใจอ้างถึงในชื่อเรื่องก็คือเรื่องนั้นแหละ
    • พวกเขาอยากให้พวกคุณ หรือพวกเรา เขียนทุกอย่างใหม่ด้วย Rust ไม่ใช่ให้ตัวเองทำ
    • คนสายภาษาโปรแกรมมิงก็ชอบ bootstrapping ด้วย การเขียน Rust ด้วย Rust จึงไม่ได้ไร้เหตุผลขนาดนั้น
  • ค่อนข้างแปลกที่บ่นเรื่องการตัดสินใจที่เชื่องช้าก่อน แล้วจึงยกฟีเจอร์หลายอย่างที่เหตุผลว่ายังไม่เสถียรแทบไม่เกี่ยวกับการตัดสินใจเลย
    ตัวอย่างเช่น coroutine ติดอยู่เพราะมี edge case ที่แก้ให้ถูกต้องได้ยาก ไม่ใช่ว่ามี implementation ที่เสร็จสมบูรณ์อยู่ในคอมไพเลอร์แล้ว “แค่เปิดใช้ก็พอ” แต่เป็น implementation ที่ยังไม่สมบูรณ์ ซึ่งทำงานได้ในหลายกรณีแต่เปิดในเวอร์ชัน stable ไม่ได้ ส่วน function trait ก็มีการตัดสินใจไว้อย่างชัดเจนแล้วว่าจะไม่ทำให้เสถียรในรูปแบบปัจจุบัน เพราะเหตุผลทางเทคนิคหลายอย่างและปฏิสัมพันธ์กับฟีเจอร์ในอนาคต ถ้าย้อนเวลากลับไปได้ก็คงมีบางส่วนที่ออกแบบต่างไป แต่ส่วนใหญ่เกี่ยวข้องกับการตัดสินใจในยุคแรก ๆ ของ Rust ตอนที่ทีมและทรัพยากรยังเล็กกว่านี้มาก อาจมีแนวคิดว่าเพราะตอนนี้เลือกได้ดีกว่า จึงควรสร้างการแตกหักแบบ Rust 2.0 แต่เพราะหายนะจากการย้าย Python 2 ไป 3 ใหญ่มาก หลายคนจึงเห็นว่าทนกับมุมขรุขระไปดีกว่า ถ้าดูจดหมายข่าวรายสัปดาห์ของ Rust จะเห็นว่าการอนุมัติ RFC และการตัดสินใจทำให้เสถียรถูกจัดการทุกสัปดาห์ มีบางกรณีที่ใช้เวลานานเกินไปจริง ๆ แต่ปัญหาเรื่องคน การประสานงาน และเวลาที่ไม่พอมักแก้ยากกว่าปัญหาทางเทคนิค

  • บทความนี้ควรอ่านคำตอบใน Reddit ของ Josh Triplett ควบคู่กันด้วย หนึ่งในตัวอย่างหลักของบทความอย่าง Mutex นั้นผิดไปเลย
    https://old.reddit.com/r/rust/comments/1fpomvp/rewriting_rus...
    แก้ไข: คอมเมนต์เดียวกันอยู่ที่นี่ด้วย
    https://news.ycombinator.com/item?id=41655268

  • สิ่งแรกที่รู้สึกตอนเรียน Rust คือดูเหมือนว่าฟีเจอร์แทบทุกอย่างที่จินตนาการได้มีอยู่ในนั้นแล้ว
    ไม่ได้หมายความว่าทีม Rust ไม่เคยปฏิเสธอะไรเลย แต่ถึงอย่างนั้นผู้คนก็ยังต้องการฟีเจอร์มากขึ้น บางอย่างก็มีเหตุผล แต่บางอย่างดูเหมือนเป็นฟีเจอร์ที่นักพัฒนาแค่ 2% จะใช้ และต้องใส่เข้าไปในภาษาที่มีนักพัฒนาแค่ 1% เท่านั้นที่เข้าใจ ภาษาที่ซับซ้อนอยู่แล้วไม่จำเป็นต้องซับซ้อนขึ้นอีก Zig เรียบง่ายกว่า และน่าจะเร็วกว่า อีกทั้งมีดราม่าในชุมชนน้อยกว่ามาก อยากให้มีเงินทุนไหลไปที่ Zig มากกว่านี้

    • คุณอาจแปลกใจถ้ารู้ว่ามีฟีเจอร์จำนวนไม่น้อยที่คนทั่วไปเสนอแล้วถูกชุมชน Rust ปฏิเสธ
      Rust ไม่ได้พยายามใส่ฟีเจอร์ทุกอย่างที่เป็นไปได้ เพียงแต่ว่าถ้าไม่รู้ชัดเจนว่ามันแก้ปัญหาอะไร เช่น GAT หรือ TAIT ก็อาจรู้สึกแบบนั้นได้ Zig อาจเป็นภาษาสมัยใหม่ที่ดี แต่ถ้าเป้าหมายคือ ความปลอดภัยของหน่วยความจำ มันก็ไม่ใช่ตัวเลือก
    • ผมเป็นผู้เขียนต้นฉบับเอง ผมเห็นว่างบประมาณด้านความซับซ้อนของ Rust ถูกใช้ไปผิดที่เป็นจำนวนมาก
      ตัวอย่างเช่นปฏิสัมพันธ์ระหว่าง Pin กับ future เพิ่มความซับซ้อนให้ภาษาอย่างไม่สมเหตุสมผล และผมคิดว่าบางส่วนในนั้นไม่จำเป็น อยากให้มีภาษาคล้าย Rust ที่ไม่มี Pin เลย ตัว borrow checker เองก็น่าจะมีวิธีทำให้เรียบง่ายขึ้นได้ทั้งในด้านไวยากรณ์และ implementation แต่ยังไม่ได้คิดเป็นรูปธรรม ตอนนี้คงเปลี่ยนได้ยากเว้นแต่จะ fork ภาษา แต่ภาษาที่ใช้ borrow checker จะไม่หยุดอยู่ที่ Rust เป็นภาษาสุดท้าย ผมคาดหวังว่ารุ่นถัดไปยังมีพื้นที่ให้ปรับปรุงได้มาก โดยไม่ต้องสร้างภาษาที่ใหญ่ขึ้นกว่าเดิม
    • ถ้ามีเงินทุนมากขึ้น อาจดึงดูดคนประเภทที่ไม่ดีเข้ามาได้ ความแย่และศักยภาพในการก่อความเสียหายของคนเหล่านั้นบางทีก็เผยตัวช้าเกินไป
    • อยากรู้ว่าหมายถึงดราม่าแบบไหนในชุมชน Rust
      ผมเห็นดราม่าที่เกี่ยวข้องกับ Rust อยู่บ้าง แต่ส่วนใหญ่มักเกิดจากฝ่ายที่ต่อต้านการใช้หรือการนำ Rust ไปใช้ เช่นความวุ่นวายเรื่อง Rust for Linux เมื่อไม่นานนี้ ดูไม่เหมือนว่าเป็นเรื่องปกติภายในชุมชน แต่อาจมีอะไรที่ผมพลาดไป Zig ยอดเยี่ยม แต่ยังไม่ถึงขั้น พร้อมใช้งานจริงในโปรดักชัน
    • สุสานของฟีเจอร์ที่ถูกฝังอยู่ใน nightly นั้นจริง ๆ แล้วค่อนข้างใหญ่ แม้แต่ฟีเจอร์สำคัญอย่าง specialization ก็ยัง stuck อยู่ในนั้นไปตลอดกาล
  • ผู้เขียนคอมเมนต์ที่ลิงก์ไว้ได้วิเคราะห์ primitive สำหรับการซิงโครไนซ์ ของหลายภาษาอย่างกว้างขวาง แล้วเขียน primitive สำหรับการซิงโครไนซ์ อย่าง Mutex และ RwLock ของ Rust ขึ้นใหม่ให้ใช้ฟังก์ชันดิบของ OS ระดับล่างโดยตรงในแต่ละระบบปฏิบัติการ
    เขาใช้สิ่งอย่าง futex ของ Linux เพื่อทำให้เร็วขึ้น เล็กลง และโดยรวมดีขึ้น และในกระบวนการนั้นก็แทบจะเขียนหนังสือเรื่องการเขียนโปรแกรมขนานด้วย Rust ไปด้วย หนังสือนี้มีประโยชน์แม้กับการเขียนโปรแกรมขนานที่ไม่ใช่ Rust
    https://www.oreilly.com/library/view/rust-atomics-and/978109...
    ตลอด 7 ปีที่ผ่านมาไม่ได้เอาแต่เล่นอยู่กับฝั่ง coroutine เท่านั้น มีการเพิ่มฟีเจอร์หลายอย่างที่จำเป็นต่อการทำให้ฟังก์ชัน async, trait ที่สามารถมีฟังก์ชัน async ได้, และ AsyncWrite กับ AsyncRead เป็นมาตรฐาน และใน nightly ก็มีการ implement generator แล้วด้วย ขณะนี้ยังถกกันอยู่ว่าจะรับความซับซ้อนของ coroutine แบบทั่วไปเต็มรูปแบบหรือจะหยุดแค่ generator ฟีเจอร์บางอย่างอย่าง AsyncIterator เดินช้า แต่ก็มีงานจำนวนมากที่กำลังดำเนินอย่างคึกคัก น่าสนใจเสมอที่ฝั่งหนึ่งบอกว่าภาษาช้าเกินไป ส่วนอีกฝั่งบ่นว่าเร็วเกินไป
    ช่วงหลังมีการสำรวจการออกแบบครั้งใหญ่ในเรื่อง function trait และ effect system ด้วย และอยากหาแนวทางที่ไม่ต้องเขียนฟังก์ชันซ้ำหลายครั้งสำหรับแต่ละชุดผสมของ async, try, const การ sandbox crate ที่เป็นอันตรายไม่ใช่ปัญหาระดับภาษา แต่ต้องอาศัยการผสมกันระหว่าง verifier กับ runtime sandbox และ WebAssembly component ดูมีความเป็นไปได้มากกว่า อย่างไรก็ตาม มีความสนใจอย่างมากใน capability ตอนคอมไพล์ เช่น allocator หรือการเลือกรันไทม์ async และสมมติฐานว่าเป็นแพลตฟอร์ม 64 บิต ส่วน proc macro sandboxing ต้องการเพื่อการแคชที่ถูกต้อง ไม่ใช่เพื่อป้องกันเจตนาร้าย
    struct แบบอ้างอิงตัวเองไม่ใช่ปัญหาไวยากรณ์ แต่เป็นปัญหาที่ยากมากสำหรับ borrow checker จะจัดการ partial borrow นั้นรองรับอยู่แล้วในการ capture ของ closure แต่ประเด็นสำคัญคือจะรักษา semantic versioning ที่เสถียรอย่างไรเมื่อเปิดเผยผ่าน API สาธารณะ แนวทางอย่าง “borrow group” ที่มีชื่อดูมีโอกาสสูง ฝั่ง comptime ก็มีงานหลายทิศทาง และ RFC สำหรับเสริมความสามารถของ macro_rules ก็เพิ่งเขียนเมื่อไม่นานมานี้ impl ของ Range ก็กำลังเดินหน้าอยู่แล้ว โดยผูกกับการเปลี่ยนแปลงแบบไม่เข้ากันผ่าน edition การรวม if let กับ logical AND มีเป็นฟีเจอร์ unstable อยู่และใกล้จะ stabilize แล้ว ข้อเสนอปรับปรุงไวยากรณ์การเข้าถึง field ของ pointer ก็มีหลายข้อ แต่ยังเป็นคำถามเปิดอยู่ว่าการเพิ่มพื้นผิวของภาษาอีกจะได้มากกว่าเสียหรือไม่ ด้วย Rust edition ถ้ามีการออกแบบที่น่าเชื่อถือเพียงพอ ก็ยังมีหลายอย่างที่เปลี่ยนได้ ฟีเจอร์ unstable 700 รายการเป็นปัญหาจริง จึงต้องมีงานใหญ่ในการจัดระเบียบสิ่งที่มีโอกาสต่ำจะ stabilize

    • การพัฒนาแพ็กเกจพื้นฐานระดับ standard library ควรทำแบบนี้ ภาษาที่ใช้อยู่ตอนนี้จะยังถูกใช้ต่อไปอีกหลายสิบปี และถ้าตัดสินใจแบบ ช้าแต่ดี ในตอนนี้ ภายหลังจะประหยัดเวลาได้มากกว่ามาก
    • มีการถกเถียงกันมากเกี่ยวกับ proc macro และ build script sandboxing
      macro ที่เป็น declarative มากขึ้น, การมอบ logic ของ crate -sys ให้ shared library, cfg(version) / cfg(accessible) จะช่วยลดความจำเป็นที่ผู้ใช้ต้อง implement เองได้มาก แต่ runtime ก็ยังคงเหลืออยู่ ยิ่งคิดก็ยิ่งรู้สึกว่า ACL ของ cackle ดูเป็นแนวทางที่ขยายได้ดี สำหรับติดตามงานทั้งใน proc macro, build script และโค้ด runtime รวมถึง audit การใช้งาน dependency tree ได้ยินมาว่า cargo-redpen ก็กำลังพัฒนาเป็นเครื่องมือ audit การเรียกใช้งานเช่นกัน แต่กำลังจินตนาการถึงระดับที่สูงกว่านั้นแบบ cackle
      https://github.com/cackle-rs/cackle
    • เป็นผู้เขียนบทความต้นฉบับเอง ดีที่ได้ฟังมุมมองจากคนวงในอย่างละเอียด
      ผมเข้าใจที่ผู้คนบ่นว่า Rust เป็นภาษาขนาดใหญ่และไม่อยากให้มันใหญ่ขึ้นอีก แต่การคง async implementation ที่สุก ๆ ดิบ ๆ ในปัจจุบันไว้ไม่ได้ทำให้ภาษามีขนาดเล็กลงหรือเรียบง่ายขึ้น มันแค่ทำให้ภาษาแย่ลงเท่านั้น partial borrow ถ้าไม่ได้ทำงานใน API สาธารณะ อย่างน้อยทำงานได้ภายใน crate เดียวกันก็ยังดี นี่เป็นปัญหาที่เจอตลอดในการเขียนโปรแกรมจริง และผมก็สงสัยว่าทำไมการ sandbox crate ที่เป็นอันตรายถึงเป็นไปไม่ได้ในระดับภาษา ถ้าฟังก์ชันหนึ่งไม่มีโค้ด third-party แบบ unsafe อยู่ใน call tree และไม่ทำ system call มันก็น่าจะจัดการได้เฉพาะอาร์กิวเมนต์ที่ส่งเข้ามา ตัวแปร local และ global ในสโคปที่มีอยู่แล้วเท่านั้น ถ้าสามารถเสริมกำแพงนี้ให้ใช้เป็นขอบเขตความปลอดภัยได้ ก็ดูเหมือนจะลดความเสี่ยงของ supply chain ฝั่ง dependency ได้อย่างมาก
  • ภารกิจของ Rust ตั้งแต่แรกคือเรื่องยากในการผสมผสาน performance, safety และ expressiveness เข้าด้วยกัน และเมื่อ Mozilla ถอนตัว Rust ก็สูญเสีย founder mode ไป อีกทั้งทีมแกนหลักดั้งเดิมส่วนใหญ่ก็ออกไปแล้ว จึงไม่น่าแปลกใจที่ความคืบหน้าช้าลง
    โดยส่วนตัวผมมองว่ายังดีกว่าเดินผิดทาง

  • ถ้าเขียน Rust ใหม่ ผมคิดว่าน่าจะไปทางลดฟีเจอร์มากกว่าเพิ่มฟีเจอร์
    คล้ายกับที่ QBE ทำกับ LLVM คือให้ Rust 70% ด้วยโค้ด 10% ถ้าตัด macro กับฟีเจอร์บางอย่างที่ไม่ค่อยใช้ อาจเป็นไปได้ก็ได้
    https://c9x.me/compile/

    • ไม่ได้ตั้งใจจะดูแคลน QBE แต่เป้าหมายเริ่มแรกของ QBE คือให้ performance 90% ด้วยโค้ด 10% และภายหลังเปลี่ยนเป็น 70%
      ไม่ว่าจะเป็น Rust หรืออะไรอื่น ก่อนจะลองทำจริงก็ยากจะรู้ว่าเป็นไปได้แค่ไหน