3 điểm bởi GN⁺ 2026-02-23 | 1 bình luận | Chia sẻ qua WhatsApp
  • Giải thích cách thiết kế trong Rust tận dụng hệ thống kiểu để đảm bảo bất biến tại thời điểm biên dịch thay vì xác thực khi chạy
  • Định nghĩa các kiểu mới (newtype) như NonZeroF32, NonEmptyVec để khiến các trạng thái sai (0, vector rỗng, v.v.) trở nên không thể biểu diễn
  • Thay vì trả về lỗi bằng Option hoặc Result, bài viết siết chặt ràng buộc ở tham số hàm để chặn lỗi từ trước
  • Đưa ra các ví dụ chuyển đổi sang kiểu có ý nghĩa thông qua phân tích cú pháp như String::from_utf8 hoặc serde_json::from_str
  • Nguyên tắc thiết kế khiến trạng thái bất hợp pháp trở nên không thể biểu diễn và đẩy việc xác thực lên sớm nhất có thể giúp tăng độ ổn định và khả năng đọc của mã

1. Biểu diễn ràng buộc bằng kiểu thay vì xác thực lúc chạy

  • Trong hàm divide(a, b), nếu chia cho 0 thì sẽ xảy ra panic khi chạy
    • Có thể biểu diễn thất bại bằng cách trả về Option, nhưng điều đó làm yếu kiểu trả về
  • Định nghĩa kiểu NonZeroF32 để chỉ cho phép tạo các giá trị khác 0
    • Hàm khởi tạo có dạng fn new(n: f32) -> Option<NonZeroF32>, trả về None khi thất bại
    • Nếu định nghĩa là divide_floats(a: f32, b: NonZeroF32) thì không cần xác thực lúc chạy
  • Trách nhiệm xác thực được chuyển từ bên trong hàm sang phía nơi gọi, qua đó loại bỏ lỗi từ sớm

2. Loại bỏ xác thực trùng lặp và đơn giản hóa mã

  • Trong hàm roots(a, b, c), nếu xử lý việc kiểm tra a == 0 bằng Option thì việc xác thực bị lặp ở cả phía nơi gọi lẫn trong hàm
  • Dùng NonZeroF32 thì chỉ cần xác thực một lần, sau đó logic tiếp theo trở nên đơn giản hơn
  • Theo cùng nguyên lý, có thể định nghĩa NonEmptyVec<T> để không cho phép vector rỗng
    • Nếu get_cfg_dirs() trả về NonEmptyVec<PathBuf>, thì sau đó trong main() không cần xác thực thêm

3. Ví dụ thực tế: String và serde_json

  • String về bản chất là một kiểu mới (newtype) của Vec<u8>, và String::from_utf8 thực hiện kiểm tra tính hợp lệ
    • Sau đó có thể dùng an toàn như một chuỗi đã được đảm bảo UTF-8
  • from_str::<Sample> của serde_json phân tích JSON thành struct và đảm bảo ở thời điểm biên dịch sự hiện diện của trường và tính nhất quán kiểu
    • Mọi ràng buộc như sự tồn tại của các trường foo, bar, khớp kiểu, độ dài mảng, v.v. đều được kiểm tra ở cấp độ kiểu

4. Hai nguyên tắc của thiết kế hướng kiểu

  • Khiến trạng thái bất hợp pháp trở nên không thể biểu diễn
    • NonZeroF32 không thể biểu diễn 0, còn NonEmptyVec không thể biểu diễn trạng thái rỗng
    • Các hàm xác thực đơn giản như is_nonzero vẫn có thể biểu diễn trạng thái sai nên chưa trọn vẹn
  • Thực hiện xác thực càng sớm càng tốt
    • Nếu xác thực bị rải rác khắp mã như trong ‘Shotgun Parsing’, nó có thể dẫn đến các lỗ hổng bảo mật (CVE-2016-0752, v.v.)
    • Nếu mọi ràng buộc được kiểm tra ở bước phân tích cú pháp, logic phía sau có thể chạy an toàn

5. Chứng minh dựa trên kiểu trong Rust và ứng dụng

  • Theo tương ứng Curry-Howard, kiểu có thể được xem là mệnh đề logic, còn giá trị là chứng minh của mệnh đề đó
    • Dùng crate typenum có thể kiểm chứng các quan hệ toán học (3 + 4 = 8) tại thời điểm biên dịch
  • Có thể dùng hệ thống kiểu để chứng minh tính đúng đắn của chương trình ở thời điểm biên dịch

6. Lời khuyên áp dụng trong thực tế

  • Dù API bên ngoài chỉ yêu cầu các kiểu đơn giản như bool, i32, bên trong vẫn nên biểu diễn bằng enum hoặc newtype có ý nghĩa
    • Ví dụ: định nghĩa LightBulbState { On, Off } và triển khai From<LightBulbState> for bool
  • Nếu có các hàm xác thực đơn giản như verify() hoặc do_something_fallible(), hãy cân nhắc chuyển đổi sang kiểu có cấu trúc thông qua phân tích cú pháp
  • Với các hàm không có tác dụng phụ, có thể dùng kiểu để biểu diễn trạng thái cố ý là không thể xảy ra như Result<Infallible, MyError>

7. Kết luận

  • Nếu dùng hệ thống kiểu của Rust như một công cụ xác thực, độ rõ ràng và độ ổn định của mã sẽ được cải thiện
  • Nhiều công cụ trong hệ sinh thái Rust như Vec, sqlx, bon đã tận dụng thiết kế dựa trên kiểu
  • Không phải mọi vấn đề đều có thể giải quyết bằng kiểu, nhưng cách tiếp cận đưa logic xác thực lên cấp độ kiểu sẽ tăng khả năng bảo trì và độ an toàn
  • Khuyến khích tận dụng tối đa hệ thống kiểu mạnh của Rust để viết mã mà trình biên dịch có thể bắt lỗi giúp bạn

1 bình luận

 
GN⁺ 2026-02-23
Ý kiến Hacker News
  • Ví dụ chia cho 0 được dùng trong bài này không phù hợp để giải thích nguyên tắc “Parse, Don’t Validate”
    Cốt lõi của nguyên tắc này nằm ở các hàm chuyển dữ liệu không đáng tin cậy thành kiểu dữ liệu đúng về mặt cấu trúc
    Bài viết "Names are not type safety" của Alexis King cũng nêu rõ rằng mẫu newtype không thể đảm bảo hoàn toàn tính “correct by construction”
    Khi hệ thống kiểu không thể biểu đạt trực tiếp bất biến, cách tiếp cận thực tế là dùng kiểu trừu tượng với smart constructor để mô phỏng parser
    Ví dụ thứ hai là non-empty vec là trường hợp tốt hơn nhiều, vì nó đảm bảo trong hệ thống kiểu rằng “luôn tồn tại ít nhất một phần tử”

    • Cách “parse, don’t validate” dựa trên newtype trong thực tế vẫn rất hữu ích
      Khi không biết chuỗi đến từ đâu, giá trị được đóng gói sẽ làm tăng độ tin cậy lên đáng kể
      Để đạt được correctness-by-construction hoàn chỉnh thì cần hệ thống kiểu phụ thuộc, nhưng cũng có những phương án nhẹ hơn như pattern types của Rust
      Ví dụ có thể giới hạn phạm vi như i8 is 0..100 hoặc biểu diễn slice không rỗng bằng [T] is [_, ..]
      Tuy vậy, non-empty list ở dạng (T, Vec<T>) là ví dụ cho thấy sự va chạm giữa tính thực dụng và tính thuần lý thuyết, vì nó có nhiều hạn chế nếu muốn xử lý như vector
    • ‘correct by construction’ là mục tiêu tối hậu
      Những kiểu như NonZeroU32 tuy đơn giản, nhưng sức mạnh thực sự nằm ở việc thiết kế toàn bộ logic miền nghiệp vụ bằng kiểu dữ liệu để compiler đóng vai trò người gác cổng
      Làm như vậy sẽ chuyển gánh nặng debug từ runtime sang thời điểm thiết kế
    • Có thể tìm thêm tài liệu liên quan với từ khóa “make invalid states impossible/unrepresentable”
      Ví dụ có thể tham khảo "Domain Modeling Made Functional"video liên quan
    • Ví dụ chia cho 0 là một trường hợp tách biệt mối quan tâm sai cách
      Thay vì cố bọc ở mức này, nếu bọc hành vi của các hàm số học như overflow thì sẽ thấy sự khác biệt rõ hơn
  • Tôi đã tổng hợp các liên kết thảo luận gần đây liên quan
    Parse, Don't Validate (2019) (tháng 2 năm 2026, 172 bình luận)
    Parse, Don’t Validate – Some C Safety Tips (tháng 7 năm 2025, 73 bình luận)
    Parse, Don't Validate (2019) (tháng 7 năm 2024, 102 bình luận) v.v.
    Chỉ chia sẻ để tham khảo

  • Cách tiếp cận parsing over validation có giới hạn khi không thể biết hết mọi trường hợp của thực tế
    Với những thứ như định dạng tệp, việc thất bại càng sớm càng tốt là hợp lý, nhưng cần thận trọng khi áp dụng vào logic nghiệp vụ hoặc mô hình hóa chuyển trạng thái
    Khi yêu cầu thực tế thay đổi, hệ thống có thể không tiếp nhận được và cuối cùng người dùng sẽ tìm cách lách qua

  • Ở các ngôn ngữ khác có thể tiến xa hơn với dependent typing
    Ví dụ, get_elem_at_index(array, index) có thể đảm bảo phạm vi chỉ số tại thời điểm biên dịch ngay cả khi chưa biết trước độ dài mảng
    Các kiểu Vect n aFin n của Idris là ví dụ cho điều đó

    • Trong Rust cũng có thư viện dùng macro để mô phỏng dependent type
      Ví dụ: anodized (video giới thiệu)
    • Nếu độ dài mảng được đọc từ stdin thì không thể biết tại thời điểm biên dịch, nên kiểu kiểm chứng này chỉ giới hạn trong các trường hợp có thông tin tĩnh
    • Mong rằng những tính năng như vậy sẽ trở nên phổ biến hơn
  • Cũng có cách tiếp cận đặt nhiều hàm lên một kiểu dữ liệu
    Đó là cách như Clojure, nơi mọi dữ liệu được biểu diễn bằng một map và toàn bộ thư viện chuẩn có thể thao tác trên nó

    • Giữa câu nói của Perlis “100 hàm cho một cấu trúc dữ liệu” và “Parse, Don’t Validate” tồn tại một sự căng thẳng
      Có thể đưa các bất biến quan trọng vào kiểu, hoặc biểu diễn chúng bằng các hàm đơn giản
      Ngay cả trong ngôn ngữ kiểu động cũng có các thói quen thiết kế tạo ra hiệu quả tương tự
    • Đây không hẳn là một phương án thay thế thuần túy mà là trade-off
      Đầu vào từ bên ngoài cuối cùng vẫn phải được parse, nên nó không thể thay thế hoàn toàn
    • Nghe có vẻ giống lời phê bình “stringly typed language”, nhưng thực tế đây là quá trình tinh chỉnh dần hình dạng dữ liệu
    • Sự cân bằng là quan trọng
      Trong hệ thống kiểu cấu trúc có thể mô phỏng nominal type bằng branding, và ngược lại cũng được, nhưng không tiện về mặt công thái học
      Cuối cùng, trộn cả hai cách một cách phù hợp là thực tế nhất
  • Cuộc thảo luận này gợi nhớ đến tính năng concepts của C++
    Trong Concept-based Generic Programming của Bjarne Stroustrup có ví dụ về việc tự động kiểm chứng chuyển đổi số nguyên
    Các kiểu như Number<unsigned int> hay Number<char> sẽ ném ngoại lệ nếu vượt ra ngoài phạm vi

  • Ví dụ try_roots trong bài thực ra là một phản ví dụ
    Nếu muốn biểu diễn ràng buộc b^2 - 4ac >= 0 bằng kiểu trong Rust thì sẽ trở nên rất phức tạp
    Trong trường hợp như vậy, chỉ cần trả về Option và kiểm tra bên trong hàm sẽ hợp lý hơn
    Phần lớn việc kiểm chứng liên quan đến tương tác giữa nhiều giá trị, nên giải quyết bằng “parsing” khá bất tiện

    • Khi tính hợp lệ của đầu vào phụ thuộc vào quan hệ giữa nhiều đối số, cuối cùng vẫn phải gộp lại thành dạng như fn(abc: ValidABC)
  • Mẫu này cũng rất phù hợp với thiết kế API
    Thay vì kiểm chứng yêu cầu JSON, có thể parse ngay từ đầu thành struct được đảm bảo bằng kiểu, nhờ đó không cần kiểm tra trùng lặp trong logic phía sau
    Có thể triển khai dễ dàng bằng tổ hợp serde + custom deserializer của Rust
    Tôi từng thấy trường hợp dùng cách này giúp giảm 60% mã xử lý lỗi

    • Trong Go cũng có thử áp dụng, nhưng do lạm dụng con trỏthiếu kiểu đại số, mã trở nên hơi dài dòng
  • Cùng một triết lý đó cũng được áp dụng vào hệ thống thiết kế UI
    Thay vì kiểm tra CSS về sau, người ta định nghĩa các kiểu chỉ cho phép bố cục theo đơn vị lưới, để các margin tùy ý như 13px trở thành lỗi biên dịch
    Làm vậy giúp thiết kế được giữ một cách tất định

    • Có người hỏi họ đang dùng công cụ nào
  • records + pattern matching của C# khá gần với cách tiếp cận này
    discriminated unions của F# còn mạnh hơn, vì với Result<'T,'Error> có thể khiến trạng thái không hợp lệ trở nên không thể biểu diễn
    C# cũng sẽ gọn gàng hơn nhiều nếu sau này có DU gốc