- เปรียบเทียบว่าแต่ละภาษาพยายามแก้ปัญหาอะไร โดยเน้นที่ ความต่างด้านปรัชญาและคุณค่าที่แต่ละภาษายึดถือ
- Go ถูกอธิบายว่าเป็นภาษาที่ให้ความสำคัญกับความเรียบง่ายและเสถียรภาพ ลดฟีเจอร์ให้เหลือน้อยเพื่อให้การทำงานร่วมกันและการบำรุงรักษาทำได้ง่าย
- Rust มุ่งแสวงหาทั้งความปลอดภัยและประสิทธิภาพไปพร้อมกัน และรับประกัน ความปลอดภัยของหน่วยความจำ ด้วยระบบชนิดข้อมูลและโครงสร้าง trait ที่ซับซ้อน
- Zig ถูกพรรณนาว่าเป็นภาษาทดลองที่มอบอำนาจควบคุมทั้งหมดให้แก่นักพัฒนา ผ่านการจัดการหน่วยความจำด้วยตนเองและ การออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง
- แนวทางที่แตกต่างกันของทั้งสามภาษาเผยให้เห็น ระบบคุณค่าที่ภาษาโปรแกรมพยายามทำให้เป็นจริง และเกณฑ์ในการเลือกก็คือ นักพัฒนาเห็นพ้องกับปรัชญาแบบใด
มุมมองในการเปรียบเทียบภาษา
- ผู้เขียนต้องการทำความเข้าใจ ระบบคุณค่าของแต่ละภาษา ผ่านการทดลองภาษาใหม่ ไม่ใช่ภาษาที่ใช้ในที่ทำงาน
- เน้นว่าประเด็นสำคัญไม่ใช่แค่การเทียบรายการฟีเจอร์ แต่คือ ภาษานั้นเลือก trade-off แบบใด
- Go, Rust และ Zig มีส่วนที่ซ้อนทับกันมากในเชิงความสามารถ แต่ คุณค่าที่ผู้ออกแบบให้ความสำคัญ ต่างกัน
- เมื่อเข้าใจปรัชญาของแต่ละภาษา ก็จะประเมินได้ว่าเหมาะกับสภาพแวดล้อมและเป้าหมายแบบใด
Go — ภาษาที่เน้นความเรียบง่ายและการทำงานร่วมกัน
- Go โดดเด่นด้วย มินิมัลลิสม์ และมีลักษณะที่ว่า “สามารถเก็บทั้งภาษาไว้ในหัวได้”
- generics ถูกเพิ่มเข้ามาหลังผ่านไป 12 ปี และฟีเจอร์อย่าง tagged union หรือ syntax sugar สำหรับการจัดการ error ก็ยังไม่มี
- มีความระมัดระวังอย่างมากในการเพิ่มฟีเจอร์ ทำให้มี boilerplate code มาก แต่แลกกับ เสถียรภาพและความอ่านง่ายของภาษา ที่สูง
- slice ของ Go ครอบคลุมบทบาทของ
Vec<T> ใน Rust หรือ ArrayList ใน Zig และให้รันไทม์จัดการตำแหน่งหน่วยความจำโดยอัตโนมัติ
- ออกแบบมาจากความไม่พอใจต่อความซับซ้อนและความล่าช้าในการคอมไพล์ของ C++ โดยมีเป้าหมายคือ ความเรียบง่ายและการคอมไพล์ที่รวดเร็ว
- ให้ความสำคัญกับ ประสิทธิภาพของการทำงานร่วมกันในสภาพแวดล้อมองค์กร โดยให้ความชัดเจนของโค้ดและความสม่ำเสมอมาก่อนฟีเจอร์ที่ซับซ้อน
Rust — ซับซ้อนแต่ทรงพลังด้านความปลอดภัยและประสิทธิภาพ
- Rust ชูแนวคิด “zero-cost abstraction” และเป็น ภาษาสาย maximalist ที่รวมแนวคิดหลากหลายเข้าด้วยกัน
- เหตุผลที่เรียนรู้ยากคือมี ความหนาแน่นของแนวคิด สูง พร้อมระบบชนิดข้อมูลและโครงสร้าง trait ที่ซับซ้อน
- เป้าหมายหลักของ Rust คือการทำให้ ประสิทธิภาพและความปลอดภัยของหน่วยความจำ อยู่ร่วมกันได้
- มีการตรวจสอบตั้งแต่ตอนคอมไพล์เพื่อป้องกัน UB (Undefined Behavior)
- ปิดกั้น พฤติกรรมที่คาดเดาไม่ได้ จากการอ้างอิง pointer ที่ผิดพลาดหรือการ free ซ้ำ
- เพื่อให้คอมไพเลอร์เข้าใจพฤติกรรมขณะรันของโค้ด นักพัฒนาจึงต้อง กำหนดชนิดข้อมูลและ trait อย่างชัดเจน
- โครงสร้างลักษณะนี้ทำให้ ความน่าเชื่อถือของโค้ดจากผู้อื่น สูง และช่วยให้ ecosystem ของไลบรารี คงความคึกคักอยู่ได้
Zig — การควบคุมอย่างสมบูรณ์และการออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง
- Zig เป็น ภาษาที่ใหม่ที่สุด ในบรรดาสามภาษา ปัจจุบันอยู่ในช่วงเวอร์ชัน 0.14 และแทบไม่มีเอกสารของ standard library
- ใช้ การจัดการหน่วยความจำแบบแมนนวล ทำให้นักพัฒนาต้องเรียก
alloc() เองและต้องเลือก allocator ด้วยตนเอง
- ต่างจาก Rust หรือ Go ตรงที่ สร้างตัวแปร global ได้ง่าย และจะตรวจจับ “illegal behavior” ระหว่างรันไทม์แล้วหยุดโปรแกรม
- มี โหมดรีลีส 4 แบบ ที่เลือกได้ตอน build เพื่อปรับสมดุลระหว่างประสิทธิภาพกับเสถียรภาพ
- จงใจตัดฟีเจอร์ของ การเขียนโปรแกรมเชิงวัตถุ (OOP) ออก
- ไม่มี private field หรือ dynamic dispatch และแม้แต่
std.mem.Allocator ก็ไม่ได้ถูกทำเป็น interface
- แต่หันไปมุ่งสู่ การออกแบบที่ยึดข้อมูลเป็นศูนย์กลาง (data-oriented design) แทน
- แม้แต่การจัดการหน่วยความจำก็ยังแนะนำโครงสร้างแบบ จัดสรรและคืนหน่วยความจำเป็นบล็อกใหญ่ตามช่วงเวลา แทนการจัดการรายวัตถุอย่างละเอียดด้วยแนวทาง RAII
- Zig ถูกพรรณนาว่าเป็นภาษาที่มี นิสัยรักอิสระและขบถต่อกระแสหลัก โดย ตัดวิธีคิดแบบ OOP ออกและขยายการควบคุมโดยนักพัฒนาให้ถึงขีดสุด
- ปัจจุบันทีมงานกำลังมุ่งเน้นที่ การเขียน dependency ทั้งหมดขึ้นใหม่ และเวอร์ชันเสถียร (1.0) ก็ยังไม่มีกำหนด
บทสรุป — ความต่างของคุณค่าที่ภาษาสะท้อนออกมา
- Go ยึด การทำงานร่วมกันและความเรียบง่าย, Rust ยึด ความปลอดภัยและประสิทธิภาพ, ส่วน Zig ยึด เสรีภาพและอำนาจในการควบคุม เป็นคุณค่าหลัก
- ความต่างของทั้งสามภาษาไม่ใช่แค่การเปรียบเทียบฟีเจอร์ แต่สะท้อน การเลือกเชิงปรัชญาต่อการพัฒนาซอฟต์แวร์
- นักพัฒนาจะเลือกภาษาไปตามว่า ตนเองเห็นพ้องกับคุณค่าแบบใด
1 ความคิดเห็น
ความคิดเห็นใน Hacker News
ใน Rust การสร้าง mutable global variable ไม่ใช่เรื่องยาก
เพียงแค่ต้องใช้
unsafeหรือสมาร์ตพอยน์เตอร์ที่มีการซิงโครไนซ์เพราะ Rust เป็น re-entrant โดยพื้นฐาน และรับประกันความปลอดภัยของเธรดตั้งแต่คอมไพล์ไทม์
ถ้าไม่ได้กังวลเรื่องความปลอดภัยของเธรดแบบสถิต ก็ทำให้มันง่ายเหมือนใน Zig หรือ C ได้
ความต่างคือ Rust มี เครื่องมือรับประกัน เกี่ยวกับพฤติกรรมตอนรันไทม์ของโค้ดมากกว่า
เวลากลับไปใช้ภาษาอื่นแล้วเห็นคนใช้สิ่งนี้กันแบบไม่คิดอะไร ก็รู้สึกเหมือนเป็นเรื่องบ้าบอในแง่ของ ความปลอดภัย
แต่พอ “เรื่องง่ายๆ” แบบนี้สะสมกันเข้า มันก็ไม่ง่ายอีกต่อไป
Rust ข้ามเส้นนั้นไปแล้ว และตอนนี้มัน ไม่ trivial เลย
ถ้าได้ก็ดูน่าสนใจกว่า C
และก็อยากรู้ด้วยว่ากรณีที่ตัวแปรสองตัวต้องถูกล็อกพร้อมกันเสมอจะจัดการอย่างไร
เวลาไล่ดีบัก สุดท้ายต้นตอของปัญหาก็มักจะอยู่ตรงนั้นเสมอ
สำหรับบทความที่ชี้เรื่องความหนาแน่นของแนวคิดใน Rust ผมคิดว่าในทางปฏิบัติ แค่รู้ 5% ก็ใช้งานแบบ productive ได้แล้ว
ผมใช้ Rust มานานกว่า 12 ปี แต่ไม่เคยต้องใช้
#[fundamental]เลยสักครั้งRust ก็ทำ arena allocation ได้ และมีแนวคิดเรื่อง allocator อยู่
เพียงแต่มี allocator เริ่มต้นให้ และโดยทั่วไปจะใช้การจอง heap แบบชัดเจนอย่าง
Box::newmutable global ก็ทำได้แบบ
static FOO: Mutex<T> = Mutex::new(...)และจำเป็นต้องมี mutex เพื่อ ความปลอดภัยของหน่วยความจำtype system ของ Rust ถูกออกแบบมาให้รับประกันไม่ใช่แค่ memory safety แต่รวมถึง semantic safety ของโค้ดด้วย
ใน C ความซับซ้อนแบบนี้มีน้อยกว่า
ความซับซ้อน จึงเป็นประเด็นสำคัญในท้ายที่สุด
ประเด็นจึงไม่ใช่แค่ว่าทำได้หรือไม่ แต่เป็นความต่างของสไตล์การเขียนโปรแกรมพื้นฐาน
และยังมีกรณีที่ Zig Software Foundation อ้างคำพูดเกี่ยวกับ Rust ของ Asahi Lina ผิดด้วย
ท่าทีการตลาดของ Zig ที่ชอบกดภาษาอื่นให้ดูแย่ลงนั้นไม่น่าประทับใจนัก
เหตุผลที่ชอบ Zig คือมันเป็นภาษาที่จัดการกับภาวะหน่วยความจำหมดได้อย่าง สวยงาม
ทุกการจัดสรรถูกมองว่าอาจล้มเหลวได้ และต้องจัดการอย่างชัดเจน
พื้นที่สแตกก็ไม่ได้ถูกปฏิบัติราวกับเป็นเวทมนตร์ แต่คอมไพเลอร์จะวิเคราะห์ call graph เพื่ออนุมานขนาดสูงสุด
ในสภาพแวดล้อมแบบ embedded การออกแบบที่ยึด ทรัพยากรเป็นศูนย์กลาง แบบนี้เป็นสิ่งจำเป็น
สิ่งนี้แก้ไม่ได้ด้วยกลไกระดับภาษา
เพราะสุดท้ายก็ยังเผชิญปัญหาเดียวกันคือการจัดการหน่วยความจำด้วยมือ
ถ้าอย่างนั้นผมคิดว่าใช้ ภาษา GC จะดีกว่า
เพียงแต่ standard library ของ Rust ใช้ panic เมื่อเกิด OOM ดังนั้นจึงมี ecosystem แยกสำหรับการพัฒนา embedded ใน สภาพแวดล้อม no-std
slice ของ Go แตกต่างจาก
Vec<T>ของ Rustappend()จะคืนค่า slice ใหม่ และอาจแชร์หน่วยความจำเดิมหรือไม่ก็ได้ไม่มีวิธีลดขนาดหน่วยความจำ และถ้าเขียนแค่
append(s, ...)ก็จะละเลย slice ใหม่ที่คืนกลับมาGo มีท่าทีแบบ “ทำตามที่ฉันบอก” ส่วน Rust คือ “ตรวจสอบว่าคุณทำตามที่ฉันบอกจริงหรือไม่”
กล่าวคือ Go ยอมให้เกิดความผิดพลาดเพื่อแลกกับความเรียบง่าย ขณะที่ Rust เลือกทาง ลดความผิดพลาด แม้จะซับซ้อนขึ้นก็ตาม
และถ้าเขียนแค่
append(s, ...)ก็จะเกิด compile error ดังนั้นข้อความต้นฉบับจึงเป็น ข้ออ้างที่ไม่แม่นยำ เล็กน้อยGo เป็นภาษาที่ระมัดระวังมากเรื่องการเพิ่มความซับซ้อนเมื่อจะเพิ่มฟีเจอร์ใหม่
อาจเป็นเพราะไม่ค่อยมีกรณีที่ต้องส่ง growable list ไปมาโดยตรง
หลายครั้งแค่คนไม่ได้อ่านเอกสารแล้วค่อยมาแปลกใจทีหลัง
คิดว่าการจับ UB (Undefined Behavior) ของ C/C++ ด้วยการตรวจตอนรันไทม์นั้นทำได้ยากในโลกจริง
Android ก็เคยใช้ sanitizer กับทุกคอมมิต แต่เพิ่งจะเห็นว่า ช่องทางโจมตีลดลง หลังจากเปลี่ยนมาใช้ Rust
ชอบที่บทความเปรียบเทียบภาษานี้พูดถึง จุดแข็งและจุดอ่อน ของแต่ละภาษาอย่างตรงไปตรงมา
แต่ก็เสียดายที่ไม่ได้พูดถึง Raku
ในมุมมองของผม ถ้า C–Zig–C++–Rust–Go เป็นเส้นต่อเนื่องของภาษาระดับล่าง ฝั่งระดับสูงก็น่าจะเป็น Julia–R–Python–Lua–JS–PHP–Raku–WL
มันรองรับการนิยามไวยากรณ์ในระดับภาษา จึงเหมาะกับการทำ DSL หรือ parsing log
ด้วยความที่รันบน VM ประสิทธิภาพจึงต่ำกว่า แต่เหมาะกับการ ถ่ายทอดโครงสร้างของปัญหาโดยตรง
ในฐานะผู้สืบทอดของ Perl มันมุ่งไปสู่ภาษาที่ยืดหยุ่นและสอดคล้องกัน
เป็นความเข้าใจผิดถ้าคิดว่าใน Rust เมื่อฟังก์ชันคืนค่าพอยน์เตอร์แล้วจะมีการจอง heap เกิดขึ้นอัตโนมัติ
ตัวแปรภายในฟังก์ชันอยู่บนสแตก และจะหายไปเมื่อคืนค่า ดังนั้นพอยน์เตอร์จึงใช้ต่อไม่ได้
ในโหมดปลอดภัยของ Rust จะไม่สามารถ dereference พอยน์เตอร์แบบนั้นได้ และในโหมด unsafe นักพัฒนาจะเป็นผู้รับ ความรับผิดชอบในการรับประกันความถูกต้อง เอง
ดูเหมือนผู้เขียนน่าจะเข้าใจ
Box::newผิดว่าเป็น “การจัดสรรแบบปริยาย”มันดูเหมือนเป็นการเข้าใจแนวคิดผิด หรือไม่ก็เป็นการ ชี้นำผิด โดยตั้งใจ
ข้อดีที่ใหญ่ที่สุดของ Go คือ โมเดล concurrency ที่เรียบง่าย
ด้วย goroutine จึงเขียนโค้ดขนานได้ง่าย
แม้การหาจุดที่ implement interface จะทำได้ยาก แต่ความอ่านง่ายช่วยให้การทำงานเป็นทีมสะดวก
ไม่มี colored function และการสื่อสารผ่าน channel ก็เรียบง่าย ทำให้เขียนโค้ด concurrency ที่ ถูกต้อง ได้รวดเร็ว
บทความที่เกี่ยวข้อง: Structured Concurrency or Go Statement Considered Harmful
std.Ioใหม่ของ Zig มีลักษณะคล้ายกับโมเดล concurrency ของ Gogokeyword ตรงกับstd.Io.async, channel ตรงกับstd.Io.Queue, และselectตรงกับstd.Io.selectสิ่งที่ผมอยากได้คือภาษาที่มี ความเรียบง่ายของ Go แต่ผสานการจัดการ result/error/enum ของ Rust และ generics ที่ดีกว่า
ผมเคยมอง OCaml, D, Swift, Nim, Crystal มาแล้ว แต่ก็ยังไม่มีภาษาไหนครองตลาดได้จริง
ตอนนี้น่าจะลองดู Gleam แทน
หวังว่าจะมีการปรับปรุงที่ช่วยแก้ปัญหาซ้ำๆ แบบนี้ได้
ส่วน generics ก็น่าจะยังเป็นโจทย์ยากต่อไป
ชอบโทนโดยรวมของบทความนี้ เพราะสัมผัสได้ถึง ความกระตือรือร้นและความอยากรู้อยากเห็น ของนักพัฒนาหน้าใหม่
การที่ Go ไม่มี generics ไม่ได้เป็นเพียงมินิมัลลิสม์แบบเรียบง่าย แต่เป็น ผลลัพธ์ของการชั่งน้ำหนัก trade-off
lifetime ของ Rust เป็นอุปสรรคใหญ่ที่สุดสำหรับหลายคน และความใหม่ของภาษานี้ก็มาจากการผสมแนวคิดเดิมเข้าด้วยกัน
การจัดการหน่วยความจำด้วยมือของ Zig ไม่ได้มีรากจากการปฏิเสธ OOP เท่านั้น แต่ยึดตามปรัชญา Data-Oriented Design (DOD)
ปาฐกถาที่เกี่ยวข้อง: การบรรยาย DOD ของ Andrew
แก่นของปัญหาคือ “จะเลือกโปรแกรมเมอร์ช้า คอมไพเลอร์ช้า หรือรันช้า”
และดูเหมือนว่าทีม Go จะหาทาง ประนีประนอม ที่น่าพอใจได้ในที่สุด