3 คะแนน โดย GN⁺ 2026-02-23 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • อธิบายแนวทางการออกแบบใน 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 ความคิดเห็น

 
GN⁺ 2026-02-23
ความคิดเห็นจาก Hacker News
  • ตัวอย่าง การหารด้วยศูนย์ ที่ใช้ในบทความนี้ไม่ค่อยเหมาะสำหรับอธิบายหลักการ “Parse, Don’t Validate”
    แก่นของหลักการนี้อยู่ที่ฟังก์ชันที่แปลงข้อมูลที่ไม่น่าเชื่อถือให้เป็น ชนิดข้อมูลที่มีโครงสร้างถูกต้อง
    ในบทความของ Alexis King "Names are not type safety" ก็ระบุเช่นกันว่าแพตเทิร์น newtype ไม่อาจรับประกัน ‘correct by construction’ ได้อย่างสมบูรณ์
    เมื่อระบบชนิดข้อมูลไม่สามารถแสดง invariant ได้โดยตรง แนวทางที่ใช้งานได้จริงคือใช้ชนิดข้อมูลแบบนามธรรมที่เลียนแบบ parser ด้วย smart constructor
    ตัวอย่างที่สองคือ non-empty vec ซึ่งเป็นกรณีศึกษาที่ดีกว่ามาก เพราะรับประกันในระบบชนิดข้อมูลได้ว่า “มีองค์ประกอบอย่างน้อยหนึ่งตัวเสมอ”

    • “parse, don’t validate” แบบอิง newtype ก็มีประโยชน์มากในทางปฏิบัติ
      เมื่อไม่รู้ว่าสตริงมาจากที่ใด ค่าที่ถูกห่อหุ้มไว้ จะช่วยเพิ่มความน่าเชื่อถือได้มาก
      หากต้องการ correctness-by-construction แบบสมบูรณ์จะต้องใช้ ระบบชนิดข้อมูลแบบพึ่งพา (dependent type system) แต่ก็มีทางเลือกที่เบากว่าอย่าง pattern types ของ Rust
      ตัวอย่างเช่น จำกัดช่วงแบบ i8 is 0..100 หรือแสดง slice ที่ไม่ว่างด้วย [T] is [_, ..]
      อย่างไรก็ตาม non-empty list ในรูป (T, Vec<T>) เป็นตัวอย่างของความขัดแย้งระหว่างการใช้งานจริงกับความบริสุทธิ์ทางทฤษฎี เพราะมีข้อจำกัดมากหากจะใช้งานเหมือนเวกเตอร์
    • ‘correct by construction’ คือเป้าหมายสูงสุด
      ชนิดข้อมูลอย่าง NonZeroU32 นั้นเรียบง่าย แต่พลังที่แท้จริงอยู่ที่การ ออกแบบตรรกะของโดเมนทั้งหมดด้วยชนิดข้อมูล เพื่อให้คอมไพเลอร์ทำหน้าที่เป็นด่านตรวจ
      วิธีนี้ช่วยย้ายภาระการดีบักจากช่วงรันไทม์ไปสู่ช่วงออกแบบ
    • ยังสามารถค้นหาข้อมูลที่เกี่ยวข้องได้ด้วยคีย์เวิร์ด “make invalid states impossible/unrepresentable”
      ตัวอย่างที่น่าอ่านคือ "Domain Modeling Made Functional" และวิดีโอที่เกี่ยวข้อง
    • ตัวอย่างการหารด้วยศูนย์เป็นกรณีที่ การแยก concerns ผิดฝาผิดตัว
      แทนที่จะพยายามห่อหุ้มในระดับนี้ ลองห่อพฤติกรรมของฟังก์ชันคณิตศาสตร์อย่างโอเวอร์โฟลว์จะเห็นความต่างชัดเจนกว่า
  • ได้รวบรวมลิงก์การถกเถียงที่เกี่ยวข้องล่าสุดไว้
    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 คือตัวอย่างนั้น

    • ใน Rust เองก็มีไลบรารีที่ใช้แมโครเพื่อเลียนแบบ dependent types
      ตัวอย่าง: anodized (วิดีโอแนะนำ)
    • หากอ่านความยาวของอาร์เรย์มาจาก stdin ก็จะไม่รู้ตั้งแต่คอมไพล์ไทม์ ดังนั้นการตรวจสอบแบบนี้จึง จำกัดเฉพาะกรณีที่มีข้อมูลเชิงสถิต
    • หวังว่าจะได้เห็นความสามารถลักษณะนี้แพร่หลายมากขึ้น
  • ยังมีแนวทางแบบใส่หลายฟังก์ชันไว้กับชนิดข้อมูลเดียว
    เป็นวิธีแบบ Clojure ที่ ใช้ map เดียวแทนข้อมูลทั้งหมด และทำให้ทั้ง standard library สามารถจัดการมันได้

    • คำพูดของ Perlis ที่ว่า “100 functions on one data structure” กับ “Parse, Don’t Validate” มีความตึงเครียดระหว่างกัน
      คุณอาจใส่ invariant สำคัญไว้ในชนิดข้อมูล หรือจะแสดงมันด้วยฟังก์ชันธรรมดาก็ได้
      แม้ในภาษาแบบ dynamic type ก็มีนิสัยการออกแบบที่ให้ผลคล้ายกัน
    • นี่ไม่ใช่ทางเลือกทดแทนแบบบริสุทธิ์ แต่เป็น trade-off
      อินพุตจากภายนอกสุดท้ายก็ยังต้อง parse อยู่ดี จึงไม่ได้แทนที่กันทั้งหมด
    • มันฟังดูคล้ายคำวิจารณ์เรื่อง “stringly typed language” แต่จริง ๆ แล้วคือกระบวนการ ค่อย ๆ ทำให้รูปทรงของข้อมูลละเอียดชัดขึ้น
    • สิ่งสำคัญคือความสมดุล
      ในระบบชนิดข้อมูลแบบโครงสร้าง เราอาจใช้ 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% ด้วยวิธีนี้

    • ใน Go ก็พอทำได้ แต่จะค่อนข้างยืดยาวเพราะ ใช้ pointer มากเกินไป และ ไม่มี algebraic types
  • ปรัชญาเดียวกันนี้ยังถูกนำไปใช้กับ UI design system
    แทนที่จะไปตรวจ CSS ทีหลัง ก็สามารถกำหนด ชนิดข้อมูลที่อนุญาตให้จัดวางได้เฉพาะตามหน่วยกริด เพื่อทำให้ margin แบบสุ่มอย่าง 13px กลายเป็น compile error
    วิธีนี้ช่วยให้ดีไซน์คงความเป็น deterministic

    • มีคนถามว่าใช้ tooling อะไรอยู่
  • records + pattern matching ของ C# ถือว่าเข้าใกล้แนวทางนี้
    ส่วน discriminated unions ของ F# นั้นทรงพลังยิ่งกว่า เพราะใช้ Result<'T,'Error> เพื่อทำให้ สถานะที่ไม่ถูกต้องไม่สามารถแสดงออกมาได้
    และถ้า C# มี native DU ในอนาคต ทุกอย่างก็น่าจะสะอาดขึ้นมาก