เปรียบเทียบ Ada และ Rust ผ่านการแก้ปัญหา AoC
(github.com/johnperry-math)- เปรียบเทียบความแตกต่างและลักษณะเด่นที่พบจากการใช้ Ada และ Rust แก้โจทย์ Advent of Code
- วิเคราะห์ความต่างของการออกแบบภาษาและวิธีเขียนโปรแกรมจริงของทั้งสองภาษา โดยเน้นที่ ความปลอดภัย และความน่าเชื่อถือ
- ความแตกต่างปรากฏชัดจากหลายมุมมอง เช่น ไลบรารีมาตรฐาน การมีฟีเจอร์ในตัว ความต่างด้านประสิทธิภาพ และรูปแบบการจัดการข้อผิดพลาด
- อธิบายกรณีตัวอย่างที่พบจริงระหว่างการเขียนและใช้งาน ผ่านโค้ดตัวอย่างด้าน ความเป็นโมดูลาร์ เจเนอริก ลูป และการจัดการข้อผิดพลาด
- ประสบการณ์พัฒนาที่แตกต่างกันเห็นได้ชัดจากรูปแบบ static typing การจัดการอาร์เรย์ และอินเทอร์เฟซสำหรับจัดการข้อผิดพลาด
บทนำและจุดประสงค์
- ระหว่างการแก้โจทย์ Advent of Code (ต่อไปจะเรียกว่า AoC) เดิมผู้เขียนใช้เพียง Ada แต่ตั้งแต่ปี 2023 ก็เริ่มเขียนคำตอบด้วย Rust และ Modula-2 ด้วย จึงได้เปรียบเทียบโดยตรง
- เมื่อนำโซลูชันที่เดิมเน้น Ada มาเขียนใหม่ด้วย Rust จึงสัมผัสได้ถึงความแตกต่างเชิงโครงสร้างและ แนวทางเฉพาะตัว ของทั้งสองภาษา
- มีจุดประสงค์เพื่อทำให้เห็นความแตกต่างในการใช้งานจริงอย่างชัดเจน ในแง่ความปลอดภัยของโค้ด ความน่าเชื่อถือ และมุมมองด้านการออกแบบภาษา
เวอร์ชันภาษาที่ใช้ในการเปรียบเทียบ
- Ada 2022 (อ้างอิงกฎบางส่วนของ Spark 2014 ตามความจำเป็น)
- Rust 2021 (การเปรียบเทียบหลักอิงจาก Rust 1.81.0)
ฟีเจอร์ที่ไม่ได้รวมและเกณฑ์การเปรียบเทียบ
- ฟีเจอร์เด่นของแต่ละภาษา (= killer feature) มีการกล่าวถึงสั้น ๆ ในรูปคอมเมนต์ภายในเนื้อหา
- บางฟีเจอร์ไม่ได้ถูกกล่าวถึง เนื่องจากอิงจากประสบการณ์ส่วนตัวและความจำเป็นจริงของแต่ละโซลูชัน
- พยายาม ตัดความเห็นส่วนตัวออกให้มากที่สุด และมุ่งเน้นที่คุณลักษณะสำคัญ
พื้นฐานและมุมมองของผู้เขียน
- ผู้เขียนเป็นผู้ใช้ ที่ไม่ใช่เจ้าของภาษา ทั้ง Ada และ Rust โดยมีพื้นฐานจากภาษาในยุค 1980 อย่าง C/C++, Pascal, Modula-2 เป็นต้น
- ส่งผลให้ สไตล์โค้ด อาจแตกต่างจากแนวทางสมัยใหม่หรือ idiom ทั่วไป
- โค้ดอาจไม่ใช่ implementation ที่ดีที่สุดเสมอไป และในบางสถานการณ์อาจเลือกวิธีแก้ที่ตรงไปตรงมาหรือไม่เป็นไปตามธรรมเนียม
ตำแหน่งของ Ada และ Rust
- Ada ยังคงเป็นภาษาสำหรับการพัฒนาระบบ/embedded ที่มีความปลอดภัยและความน่าเชื่อถือสูงมาก โดย ให้ความสำคัญกับความอ่านง่ายของโค้ด
- Rust มีจุดแข็งด้าน memory safety และ system programming และได้รับการจัดให้เป็น “ภาษาที่นักพัฒนาชื่นชอบมากที่สุด” หลายปีในผลสำรวจนักพัฒนาของ Stack Overflow
- Ada เป็น ภาษาระดับสูงแบบอเนกประสงค์ ที่มีจุดเด่นด้านการอ่านและการบำรุงรักษา
- Rust มุ่งไปที่ การพัฒนาโปรแกรมระบบระดับล่าง พร้อมสร้างวัฒนธรรมการเขียนโปรแกรมอย่างปลอดภัยผ่านการจัดการหน่วยความจำแบบชัดเจน และชนิดข้อผิดพลาด/ตัวเลือกอย่าง error/option
เปรียบเทียบด้านความปลอดภัยและลักษณะเชิงโครงสร้าง
-
Ada
- เป็นมาตรฐาน ISO (มีสเปกที่เข้มงวด)
- ประกาศชนิดข้อมูลให้เหมาะกับลักษณะของปัญหาได้ง่าย (เช่น ช่วงค่า จำนวนหลัก ฯลฯ)
- อนุญาตให้ index ของอาร์เรย์ไม่จำเป็นต้องเป็นตัวเลข
- มีข้อกำหนดที่เข้มงวดยิ่งขึ้นใน Spark
-
Rust
- สเปกอิงจากเอกสารทางการ (Reference) และตัวคอมไพเลอร์เป็นหลัก
- การประกาศชนิดข้อมูลผูกกับ machine type (เช่น f64, u32)
- การ index อาร์เรย์เหมาะกับชนิดตัวเลขโดยธรรมชาติ
สรุปตารางฟีเจอร์/การมีในตัวที่สำคัญ
- มีความต่างในด้านการรองรับ การตรวจสอบขอบเขตอาร์เรย์, generic container, concurrency, label loop, pattern matching เป็นต้น
- Ada ใช้การจัดการข้อผิดพลาดแบบ Exception ขณะที่ Rust ใช้การคืนค่าผ่านชนิด Result/Option
- Rust โดดเด่นด้วยการรองรับ macro, pattern matching, ความเป็นฟังก์ชันแบบบริสุทธิ์ เป็นต้น
- Ada รองรับ การออกแบบแบบมีสัญญา และ Spark รองรับการตรวจสอบ DBC (Design By Contract) ตั้งแต่คอมไพล์ไทม์
- ในด้าน memory safety นั้น Rust และ Spark มีการบังคับใช้ ขณะที่ Ada อนุญาตให้ใช้ Null pointer ได้
เปรียบเทียบประสิทธิภาพและเวลาในการรัน
- โดยทั่วไป Rust มีชื่อเสียงว่า รันเร็วแต่คอมไพล์ช้า ส่วน Ada ตรงกันข้ามคือ คอมไพล์เร็วกว่า แต่การรันอาจช้ากว่าเล็กน้อยขึ้นอยู่กับการตรวจสอบระหว่างรัน
- จากผล benchmark Rust เกิด overflow ในโจทย์ day24 เพราะข้อจำกัดของชนิด f64 แต่ Ada สามารถกำหนดชนิดระดับสูงอย่าง digits 18 ได้ ทำให้เลือก machine type ที่เหมาะสมโดยอัตโนมัติและหลีกเลี่ยง overflow พร้อมแสดงประสิทธิภาพที่ดีกว่า
- Rust จำเป็นต้องใช้ f128 ที่ยังไม่เสถียรหรือไลบรารีภายนอก ขณะที่ Ada ได้เปรียบเพียงแค่กำหนดชนิดข้อมูลให้ตรงกับสเปกของคอมไพเลอร์
การจัดการไฟล์และ error handling (กรณีศึกษา 1)
การจัดการไฟล์ใน Ada
- ใช้ Ada.Text_IO เป็นพื้นฐาน
- สามารถเปิดไฟล์อย่างชัดเจน อ่านทีละบรรทัด และจัดการบรรทัดตามช่วงหรือค่าตำแหน่งที่ต้องการได้อย่างค่อนข้างตรงไปตรงมา
- เมื่อเกิดข้อผิดพลาด มักถูกจัดการด้วย exception มากกว่าการแสดงข้อความผิดพลาดที่ชัดเจน และความเป็นไปได้ที่จะเกิดข้อผิดพลาดไม่ปรากฏในลายเซ็นของฟังก์ชัน
การจัดการไฟล์ใน Rust
- ใช้ std::fs::File และ BufReader
- ตอนเปิดไฟล์จะคืนค่าเป็นชนิด Result ทำให้เห็นความเป็นไปได้ของข้อผิดพลาดอย่างชัดเจน
- ไม่รองรับการเข้าถึง index ของตัวอักษรโดยตรง ต้องจัดการผ่าน Iterator เท่านั้น
- ใช้เครื่องมือเชิงฟังก์ชันและเชิงวนซ้ำเป็นหลัก เช่น map, filter, collect, sum รวมถึงมี macro หลากหลาย (เช่น include_str!)
- สามารถประกาศข้อผิดพลาดไว้ชัดเจนในชนิดค่าที่คืน ทำให้การส่งต่อข้อผิดพลาดในระดับฟังก์ชันมีความชัดเจน
ความเป็นโมดูลาร์และเจเนอริก (กรณีศึกษา 2)
ความเป็นโมดูลาร์ของ Ada
- แยกสเปก (interface) และ implementation อย่างชัดเจนด้วย package
- เสริมความเป็นโมดูลาร์ด้วย subpackage และการใช้ไวยากรณ์ use/rename เพื่อปรับความอ่านง่าย
- รองรับ generic ของ package โดยทำให้ทั้งชนิดข้อมูล ค่าคงที่ และทั้ง subpackage เป็นแบบทั่วไปได้
ความเป็นโมดูลาร์ของ Rust
- จัดโมดูลด้วยระบบ mod/crate และตัวสร้างเอกสารช่วยทำให้การแยกสเปกและ implementation เป็นอัตโนมัติ
- มีการกำหนดสิทธิ์เข้าถึงแบบ pub/private ในเชิงประกาศ
- ใช้ use/as เพื่อ import และเปลี่ยนชื่อ
- รองรับ การทดสอบในตัว โดยสามารถประกาศ test module ในโค้ด สั่ง build และรันอัตโนมัติได้
เจเนอริก
- Ada รองรับเจเนอริกเฉพาะในระดับ package/procedure (ไม่รองรับชนิดข้อมูลเดี่ยว ๆ)
- Rust สามารถใช้เจเนอริกกับ ชนิดข้อมูลเองได้โดยตรง (แนว template)
- Ada สามารถแสดงคุณลักษณะเพิ่มเติมอย่างช่วงของชนิดข้อมูลได้ชัดเจนผ่าน range type, subtype ขณะที่ Rust ใช้ค่าคงที่ของอินสแตนซ์
เปรียบเทียบชนิด enum (กรณีศึกษา 3)
- Ada รองรับทั้ง การประกาศแบบกระชับ และทำให้ชนิดนั้นใช้งานเป็นชนิดไม่ต่อเนื่อง มีลำดับ และใช้กับลูป/ดัชนีได้โดยอัตโนมัติ
- enum ของ Rust แม้รูปแบบการประกาศจะคล้ายกัน แต่การเข้าถึงผ่าน pattern matching หรือการวนซ้ำจำเป็นต้องเขียนอย่างชัดเจนมากกว่า
บทสรุป
- Ada ให้การควบคุมที่เข้มงวดกว่าด้าน ชนิดข้อมูลเชิงสเปกระดับสูง ความสามารถในการตรวจสอบ และการตรวจเช็กขณะรัน
- Rust เหนือกว่าในด้าน สไตล์การเขียนแบบฟังก์ชัน, macro programming, การจัดการข้อผิดพลาดที่มีคอมไพเลอร์ช่วยสนับสนุน ซึ่งให้ทั้งความสะดวกในการพัฒนาและความปลอดภัย
- ในการแก้ปัญหาใช้งานจริง Ada มีจุดแข็งด้านความเข้ากันได้กับโค้ดเก่าและการบำรุงรักษา ส่วน Rust ได้เปรียบจากระบบนิเวศเครื่องมือพัฒนาสมัยใหม่และการรองรับความปลอดภัย/การทำงานขนาน
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
ลิงก์อธิบาย Nim Subranges
เอกสารที่เกี่ยวข้อง
Rust สเปก
str::as_bytesจะเหมาะกว่าเว็บไซต์ Prunt
Prunt GitHub
คอมเมนต์ HN ที่เกี่ยวข้อง
pub const SIDE_LENGTH: usize = ROW_LENGTH;ตรงไปตรงมากว่าeggsเป็นชนิด BirdSpecies ก็จะเขียนeggs[Robin],eggs[Seagull]ได้อย่างมีความหมาย แต่eggs[5]จะไม่ถูกอนุญาต ใน Rust ก็สร้างโครงสร้างข้อมูลตามต้องการได้เหมือนกัน (เช่น implementIndex<BirdSpecies>) และทำให้eggs[Robin]ใช้ได้ แต่eggs[5]เป็น error ต่างกันตรงที่ Rust ยังไม่ได้รองรับสิ่งนี้ในฐานะ “อาร์เรย์” ของภาษาตรง ๆ และเมื่อ Ada อนุญาตให้ “ชนิดที่ผู้ใช้กำหนดเองประกาศเป็นจำนวนเต็มที่เป็นบางส่วนย่อยของช่วงได้” การ indexing แบบนี้จะยิ่งแสดงพลังชัดเจน ใน Rust ตอนนี้ยังสร้าง range-limited integer แบบผู้ใช้กำหนดเองล้วน ๆ ไม่ได้ (มีแค่แบบภายในอย่าง NonZeroI16 เป็นต้น) ถ้า Rust รองรับถึงระดับนี้ได้จริงจะดีมาก