- อธิบายแนวทางการออกแบบใน Rust ที่ ใช้ระบบชนิดข้อมูลเพื่อรับประกัน invariant ตั้งแต่คอมไพล์ไทม์ แทนการตรวจสอบตอนรันไทม์
- นิยาม newtype อย่าง
NonZeroF32, NonEmptyVec เพื่อทำให้สถานะที่ไม่ถูกต้อง (เช่น 0, เวกเตอร์ว่าง) ไม่สามารถแสดงออกมาได้
- แทนที่จะคืนค่าความล้มเหลวด้วย
Option หรือ Result ก็ใช้วิธี เพิ่มข้อจำกัดที่อาร์กิวเมนต์ของฟังก์ชัน เพื่อป้องกันข้อผิดพลาดล่วงหน้า
- ยกตัวอย่างกรณีอย่าง
String::from_utf8 หรือ serde_json::from_str ที่ แปลงผ่านการ parsing ไปเป็นชนิดข้อมูลที่มีความหมาย
- หลักการออกแบบที่ทำให้สถานะที่ไม่ถูกต้องไม่สามารถแสดงออกได้ และเลื่อนการตรวจสอบให้เกิดขึ้นเร็วที่สุดเท่าที่ทำได้ ช่วยเพิ่มทั้งความเสถียรและความอ่านง่ายของโค้ด
1. แสดงข้อจำกัดด้วยชนิดข้อมูลแทนการตรวจสอบตอนรันไทม์
- ในฟังก์ชัน
divide(a, b) หากหารด้วย 0 จะเกิด runtime panic
- แม้จะสามารถคืนค่า
Option เพื่อแสดงความล้มเหลวได้ แต่ก็เป็นการทำให้ชนิดข้อมูลผลลัพธ์อ่อนลง
- กำหนดชนิด
NonZeroF32 เพื่อให้สร้างได้เฉพาะค่าที่ไม่ใช่ 0
- ตัวสร้างมีรูปแบบ
fn new(n: f32) -> Option<NonZeroF32> และคืนค่า None เมื่อสร้างไม่สำเร็จ
- หากนิยามเป็น
divide_floats(a: f32, b: NonZeroF32) ก็ไม่จำเป็นต้องตรวจสอบตอนรันไทม์
- เป็นการ ย้ายความรับผิดชอบในการตรวจสอบจากภายในฟังก์ชันไปยังฝั่งผู้เรียกใช้ เพื่อตัดข้อผิดพลาดออกไปตั้งแต่ต้น
2. ลดการตรวจสอบซ้ำซ้อนและทำให้โค้ดง่ายขึ้น
- ในฟังก์ชัน
roots(a, b, c) หากตรวจสอบ a == 0 ด้วย Option จะเกิดการตรวจสอบซ้ำทั้งฝั่งผู้เรียกและในตัวฟังก์ชัน
- เมื่อใช้
NonZeroF32 จะ ตรวจสอบเพียงครั้งเดียว และลอจิกหลังจากนั้นจะเรียบง่ายขึ้น
- หลักการเดียวกันนี้ใช้กับการนิยาม
NonEmptyVec<T> เพื่อไม่อนุญาตให้มีเวกเตอร์ว่าง
- หาก
get_cfg_dirs() คืนค่า NonEmptyVec<PathBuf> ก็ไม่ต้องมีการตรวจสอบเพิ่มเติมใน main() อีก
3. กรณีใช้งานจริง: String และ serde_json
String ภายในคือ newtype ของ Vec<u8> และ String::from_utf8 จะทำการตรวจสอบความถูกต้อง
- หลังจากนั้นก็สามารถใช้งานได้อย่างปลอดภัยในฐานะสตริงที่รับประกันว่าเป็น UTF-8
serde_json ของ from_str::<Sample> จะ parse JSON เป็น struct เพื่อ รับประกันการมีอยู่ของฟิลด์และความสอดคล้องของชนิดข้อมูลตั้งแต่คอมไพล์ไทม์
- ข้อจำกัดทั้งหมดอย่างการมีอยู่ของฟิลด์
foo, bar, การตรงกันของชนิดข้อมูล และความยาวของอาร์เรย์ จะถูกตรวจสอบในระดับชนิดข้อมูล
4. หลักการสองข้อของการออกแบบที่ขับเคลื่อนด้วยชนิดข้อมูล
- ทำให้สถานะที่ไม่ถูกต้องไม่สามารถแสดงออกได้
NonZeroF32 ไม่สามารถแทนค่า 0 ได้ และ NonEmptyVec ไม่สามารถแทนสถานะว่างได้
- ฟังก์ชันตรวจสอบธรรมดาอย่าง
is_nonzero ยังเปิดทางให้แสดงสถานะที่ไม่ถูกต้องได้อยู่ จึงยังไม่สมบูรณ์
- ตรวจสอบให้เร็วที่สุดเท่าที่เป็นไปได้
- หากการตรวจสอบกระจายอยู่ทั่วโค้ดแบบ ‘Shotgun Parsing’ ก็อาจนำไปสู่ช่องโหว่ด้านความปลอดภัยได้ (เช่น CVE-2016-0752)
- หากตรวจสอบข้อจำกัดทั้งหมดตั้งแต่ขั้นตอน parsing ลอจิกหลังจากนั้นก็สามารถทำงานได้อย่างปลอดภัย
5. การพิสูจน์ด้วยชนิดข้อมูลและการประยุกต์ใช้ใน Rust
- ตามแนวคิด Curry-Howard correspondence ชนิดข้อมูลมองได้ว่าเป็นประพจน์เชิงตรรกะ และค่าคือบทพิสูจน์ของมัน
- หากใช้ crate
typenum ก็สามารถตรวจสอบความสัมพันธ์ทางคณิตศาสตร์อย่าง 3 + 4 = 8 ได้ตั้งแต่คอมไพล์ไทม์
- ระบบชนิดข้อมูลสามารถใช้ พิสูจน์ความถูกต้องของโปรแกรมตั้งแต่ขั้นตอนคอมไพล์ ได้
6. คำแนะนำในการนำไปใช้จริง
- ถึงแม้ external API จะต้องการชนิดพื้นฐานอย่าง
bool, i32 ภายในระบบก็ควร แสดงความหมายด้วย enum หรือ newtype ที่เหมาะสม
- เช่น นิยาม
LightBulbState { On, Off } และ implement From<LightBulbState> for bool
- หากมีฟังก์ชันตรวจสอบธรรมดาอย่าง
verify() หรือ do_something_fallible() ก็ควร พิจารณาแปลงผ่าน parsing ไปเป็นชนิดข้อมูลแบบมีโครงสร้าง
- หากเป็นฟังก์ชันที่ไม่มีผลข้างเคียง ก็สามารถใช้
Result<Infallible, MyError> เพื่อ แสดงสถานะที่ตั้งใจให้เป็นไปไม่ได้ด้วยชนิดข้อมูล
7. บทสรุป
- หากใช้ ระบบชนิดข้อมูลของ Rust เป็นเครื่องมือตรวจสอบ จะช่วยเพิ่มทั้งความชัดเจนและความเสถียรของโค้ด
- เครื่องมือหลายตัวใน ecosystem ของ Rust เช่น
Vec, sqlx, bon ได้นำแนวทางออกแบบบนฐานชนิดข้อมูลมาใช้แล้ว
- แม้จะไม่สามารถแก้ทุกปัญหาด้วยชนิดข้อมูลได้ แต่ แนวทางดึงลอจิกการตรวจสอบขึ้นมาไว้ในระดับชนิดข้อมูล ช่วยเพิ่มทั้งความสามารถในการบำรุงรักษาและความปลอดภัย
- แนะนำให้ใช้ระบบชนิดข้อมูลอันทรงพลังของ Rust ให้เต็มที่ เพื่อ เขียนโค้ดที่ให้คอมไพเลอร์ช่วยจับข้อผิดพลาด
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
ตัวอย่าง การหารด้วยศูนย์ ที่ใช้ในบทความนี้ไม่ค่อยเหมาะสำหรับอธิบายหลักการ “Parse, Don’t Validate”
แก่นของหลักการนี้อยู่ที่ฟังก์ชันที่แปลงข้อมูลที่ไม่น่าเชื่อถือให้เป็น ชนิดข้อมูลที่มีโครงสร้างถูกต้อง
ในบทความของ Alexis King "Names are not type safety" ก็ระบุเช่นกันว่าแพตเทิร์น
newtypeไม่อาจรับประกัน ‘correct by construction’ ได้อย่างสมบูรณ์เมื่อระบบชนิดข้อมูลไม่สามารถแสดง invariant ได้โดยตรง แนวทางที่ใช้งานได้จริงคือใช้ชนิดข้อมูลแบบนามธรรมที่เลียนแบบ parser ด้วย smart constructor
ตัวอย่างที่สองคือ non-empty vec ซึ่งเป็นกรณีศึกษาที่ดีกว่ามาก เพราะรับประกันในระบบชนิดข้อมูลได้ว่า “มีองค์ประกอบอย่างน้อยหนึ่งตัวเสมอ”
newtypeก็มีประโยชน์มากในทางปฏิบัติเมื่อไม่รู้ว่าสตริงมาจากที่ใด ค่าที่ถูกห่อหุ้มไว้ จะช่วยเพิ่มความน่าเชื่อถือได้มาก
หากต้องการ correctness-by-construction แบบสมบูรณ์จะต้องใช้ ระบบชนิดข้อมูลแบบพึ่งพา (dependent type system) แต่ก็มีทางเลือกที่เบากว่าอย่าง pattern types ของ Rust
ตัวอย่างเช่น จำกัดช่วงแบบ
i8 is 0..100หรือแสดง slice ที่ไม่ว่างด้วย[T] is [_, ..]อย่างไรก็ตาม non-empty list ในรูป
(T, Vec<T>)เป็นตัวอย่างของความขัดแย้งระหว่างการใช้งานจริงกับความบริสุทธิ์ทางทฤษฎี เพราะมีข้อจำกัดมากหากจะใช้งานเหมือนเวกเตอร์ชนิดข้อมูลอย่าง
NonZeroU32นั้นเรียบง่าย แต่พลังที่แท้จริงอยู่ที่การ ออกแบบตรรกะของโดเมนทั้งหมดด้วยชนิดข้อมูล เพื่อให้คอมไพเลอร์ทำหน้าที่เป็นด่านตรวจวิธีนี้ช่วยย้ายภาระการดีบักจากช่วงรันไทม์ไปสู่ช่วงออกแบบ
ตัวอย่างที่น่าอ่านคือ "Domain Modeling Made Functional" และวิดีโอที่เกี่ยวข้อง
แทนที่จะพยายามห่อหุ้มในระดับนี้ ลองห่อพฤติกรรมของฟังก์ชันคณิตศาสตร์อย่างโอเวอร์โฟลว์จะเห็นความต่างชัดเจนกว่า
ได้รวบรวมลิงก์การถกเถียงที่เกี่ยวข้องล่าสุดไว้
Parse, Don't Validate (2019) (กุมภาพันธ์ 2026, 172 ความเห็น)
Parse, Don’t Validate – Some C Safety Tips (กรกฎาคม 2025, 73 ความเห็น)
Parse, Don't Validate (2019) (กรกฎาคม 2024, 102 ความเห็น) เป็นต้น
แค่แชร์ไว้เพื่อใช้อ้างอิงเท่านั้น
แนวทางแบบ Parsing over validation มีข้อจำกัดเมื่อเราไม่อาจรู้ทุกกรณีของโลกจริงได้
กับสิ่งอย่างฟอร์แมตไฟล์ การทำให้ล้มเหลวให้เร็วที่สุดเป็นเรื่องดี แต่ต้องระวังเมื่อนำไปใช้กับ ตรรกะธุรกิจ หรือการทำโมเดลสถานะเปลี่ยนผ่าน
เมื่อความต้องการในโลกจริงเปลี่ยน ระบบอาจรองรับไม่ได้ และสุดท้ายผู้ใช้ก็จะหาทางอ้อมเอง
ในภาษาอื่นสามารถไปได้ไกลกว่านี้ด้วย dependent typing
ตัวอย่างเช่น
get_elem_at_index(array, index)สามารถรับประกันช่วงของดัชนีได้ตั้งแต่คอมไพล์ไทม์ แม้จะไม่รู้ความยาวของอาร์เรย์ล่วงหน้าชนิดข้อมูล
Vect n aและFin nของ Idris คือตัวอย่างนั้นตัวอย่าง: anodized (วิดีโอแนะนำ)
ยังมีแนวทางแบบใส่หลายฟังก์ชันไว้กับชนิดข้อมูลเดียว
เป็นวิธีแบบ Clojure ที่ ใช้ map เดียวแทนข้อมูลทั้งหมด และทำให้ทั้ง standard library สามารถจัดการมันได้
คุณอาจใส่ invariant สำคัญไว้ในชนิดข้อมูล หรือจะแสดงมันด้วยฟังก์ชันธรรมดาก็ได้
แม้ในภาษาแบบ dynamic type ก็มีนิสัยการออกแบบที่ให้ผลคล้ายกัน
อินพุตจากภายนอกสุดท้ายก็ยังต้อง parse อยู่ดี จึงไม่ได้แทนที่กันทั้งหมด
ในระบบชนิดข้อมูลแบบโครงสร้าง เราอาจใช้ branding เพื่อเลียนแบบ nominal type และในทางกลับกันก็ได้ แต่ไม่ค่อยเป็นมิตรต่อการใช้งาน
สุดท้ายแล้วการผสมสองแนวทางอย่างเหมาะสมคือทางที่สมจริงที่สุด
ประเด็นนี้ทำให้นึกถึงฟีเจอร์ concepts ของ C++
ใน Concept-based Generic Programming ของ Bjarne Stroustrup มีตัวอย่างการตรวจสอบการแปลงจำนวนเต็มแบบอัตโนมัติ
โดยชนิดข้อมูลอย่าง
Number<unsigned int>หรือNumber<char>จะโยนข้อยกเว้นเมื่อค่าเกินช่วงตัวอย่าง
try_rootsในบทความจริง ๆ แล้วเป็น ตัวอย่างโต้แย้งหากจะใช้ชนิดข้อมูลเพื่อแสดงข้อจำกัด
b^2 - 4ac >= 0ใน Rust จะซับซ้อนมากในกรณีเช่นนี้ การคืนค่า
Optionและตรวจสอบภายในฟังก์ชันจะสมเหตุสมผลกว่าการตรวจสอบส่วนใหญ่มักเกี่ยวข้องกับปฏิสัมพันธ์ระหว่างค่าหลายตัว จึงไม่สะดวกนักที่จะจัดการด้วย “การ parse”
fn(abc: ValidABC)แพตเทิร์นนี้เข้ากับ การออกแบบ API ได้ดีมาก
แทนที่จะตรวจสอบคำขอ JSON เราสามารถ parse ให้เป็น struct ที่ชนิดข้อมูลรับประกันความถูกต้องตั้งแต่แรก ซึ่งทำให้ตรรกะถัดไปไม่ต้องตรวจสอบซ้ำ
ทำได้ไม่ยากด้วยการจับคู่ระหว่าง serde + custom deserializer ของ Rust
เคยเห็นกรณีจริงที่โค้ด จัดการข้อผิดพลาดลดลง 60% ด้วยวิธีนี้
ปรัชญาเดียวกันนี้ยังถูกนำไปใช้กับ UI design system
แทนที่จะไปตรวจ CSS ทีหลัง ก็สามารถกำหนด ชนิดข้อมูลที่อนุญาตให้จัดวางได้เฉพาะตามหน่วยกริด เพื่อทำให้ margin แบบสุ่มอย่าง 13px กลายเป็น compile error
วิธีนี้ช่วยให้ดีไซน์คงความเป็น deterministic
records + pattern matching ของ C# ถือว่าเข้าใกล้แนวทางนี้
ส่วน discriminated unions ของ F# นั้นทรงพลังยิ่งกว่า เพราะใช้
Result<'T,'Error>เพื่อทำให้ สถานะที่ไม่ถูกต้องไม่สามารถแสดงออกมาได้และถ้า C# มี native DU ในอนาคต ทุกอย่างก็น่าจะสะอาดขึ้นมาก