Hãy parse, đừng validate
Bản chất của thiết kế hướng kiểu
- Một khẩu hiệu đơn giản để giải thích thiết kế hướng kiểu (type-driven design): Hãy parse, đừng validate
- Khẩu hiệu này nói về cách tận dụng hệ thống kiểu để nâng cao độ an toàn và tính chính xác của mã
Miền khả năng
- Hệ thống kiểu tĩnh giúp dễ dàng xác định liệu một hàm cụ thể có thể được triển khai hay không
- Ví dụ:
foo :: Integer -> Void là không thể triển khai (Void không thể có giá trị)
- Ví dụ: hàm
head :: [a] -> a không được xác định khi danh sách rỗng
Biến hàm bộ phận thành hàm toàn phần
Quản lý kỳ vọng
- Hàm
head không thể trả về giá trị khi danh sách rỗng, nên có thể dùng kiểu Maybe để cho phép trả về Nothing
- Tuy nhiên, điều này có thể gây bất tiện khi sử dụng
Truyền tải kỳ vọng
- Dùng kiểu
NonEmpty để biểu diễn danh sách không rỗng, từ đó bảo đảm hàm head luôn trả về giá trị
- Khi dùng kiểu
NonEmpty, có thể loại bỏ các kiểm tra không cần thiết và bắt lỗi ở thời điểm biên dịch thông qua hệ thống kiểu
Sức mạnh của parse
- Khác biệt giữa parse và validate nằm ở cách thông tin được bảo toàn
- Hàm
validateNonEmpty kiểm tra rằng danh sách không rỗng, nhưng không bảo toàn thông tin đó
- Hàm
parseNonEmpty vừa kiểm tra danh sách không rỗng vừa bảo toàn thông tin bằng kiểu NonEmpty
Rủi ro của validate
- Cách tiếp cận dựa trên validate có thể dẫn đến vấn đề gọi là "shotgun parsing"
- Điều này có thể dẫn đến tình huống chương trình xử lý một phần đầu vào rồi mới phát hiện phần còn lại là không hợp lệ
- Parse chia chương trình thành hai giai đoạn: giai đoạn đầu xác nhận đầu vào hợp lệ, giai đoạn sau chỉ xử lý đầu vào hợp lệ
Parse trong thực tế
- Tập trung vào kiểu dữ liệu và làm cho kiểu chữ ký của hàm cụ thể nhất có thể
- Dùng cấu trúc dữ liệu không thể biểu diễn trạng thái bất hợp pháp, và chuyển dữ liệu sang biểu diễn cụ thể càng sớm càng tốt
- Để kiểu dữ liệu dẫn dắt mã, thay vì để mã kiểm soát kiểu dữ liệu
- Cần thận trọng khi dùng các hàm trả về
m ()
- Đừng ngại parse dữ liệu qua nhiều bước
- Tránh biểu diễn phi chuẩn hóa của dữ liệu, và nếu cần thì quản lý chúng thông qua đóng gói
- Nên dùng các kiểu dữ liệu trừu tượng để khiến validator trông giống parser
Tóm tắt, suy ngẫm, tài liệu đọc liên quan
- Việc tận dụng tối đa hệ thống kiểu của Haskell không khó, và không cần dùng các phần mở rộng ngôn ngữ hiện đại
- Ý tưởng cốt lõi là "viết hàm toàn phần", đơn giản nhưng có thể khó thực hành
- Tài liệu đọc liên quan được gợi ý gồm bài blog "Type Safety Back and Forth" của Matt Parson và bài luận "Ghosts of Departed Proofs" của Matt Noonan
Tóm tắt của GN⁺
- Bài viết này giải thích cách tận dụng hệ thống kiểu của Haskell để nâng cao độ an toàn và tính chính xác của mã
- Bài viết nhấn mạnh tầm quan trọng của việc hiểu sự khác biệt giữa parse và validate, cũng như việc xác nhận tính hợp lệ của đầu vào thông qua parse
- Điều quan trọng là dùng hệ thống kiểu để tạo các cấu trúc dữ liệu không thể biểu diễn trạng thái bất hợp pháp, và chuyển dữ liệu sang biểu diễn cụ thể càng sớm càng tốt
- Tài liệu đọc liên quan được gợi ý gồm bài blog của Matt Parson và bài luận của Matt Noonan
1 bình luận
Ý kiến trên Hacker News
Lời khuyên và bài viết này rất hữu ích
Cũng hữu ích cả với những người không dùng ngôn ngữ hàm định kiểu tĩnh
Ý tưởng này vượt qua ranh giới các paradigma
Có thể tìm thấy các khái niệm tương tự trong tài liệu hướng đối tượng thập niên 80~90, ví dụ như Design by Contract
TypeScript thường được viết theo cách tinh chỉnh kiểu ở runtime
Design by Contract có lẽ đã ảnh hưởng đến spec của Clojure (Clojure là ngôn ngữ động)
Về cơ bản đây là câu chuyện về giả định và bảo đảm (yêu cầu và cung cấp)
Khi giả định đã được xác nhận và bảo đảm đã được thiết lập, thì không cần kiểm tra lại các giả định trùng lặp ở những phần khác của chương trình
Khi thấy trong mã có việc kiểm tra lại những thuộc tính đã được bảo đảm, điều đó có thể gây bối rối và làm việc hiểu cũng như cải thiện mã trở nên khó hơn
Mẫu này cũng hoạt động tốt trong C# hiện đại và còn giúp tiết kiệm không gian
Tốt nhất là tận dụng hệ thống kiểu mạnh để khiến các trường hợp lỗi không thể được biểu diễn, điều này giúp giảm bug phần mềm
Sẽ mất thêm thời gian để suy nghĩ về vấn đề và bám theo thiết kế, nhưng trong nhiều trường hợp khoảng thời gian đó là xứng đáng
Khẩu hiệu "Parse, don’t validate" tóm tắt rất hay cho thiết kế dựa trên kiểu
Cá nhân tôi thích cách "luôn chỉ thực hiện kiểm tra tính hợp lệ trong một constructor duy nhất", vì như vậy các đối tượng không hợp lệ sẽ hoàn toàn không tồn tại
Nếu muốn sửa đổi đối tượng, nên triển khai bằng cách gọi lại chính constructor đó để cấu thành trạng thái mới
Gợi nhớ đến mục 5 của qmail, trong đó có các ý như "đừng parse" và "có giao diện tốt và giao diện người dùng"
Nếu dạy một lớp lập trình trình độ trung cấp, tôi sẽ cho sinh viên viết bài luận so sánh và đối chiếu hai đề xuất này; mỗi đề xuất đều có điều đáng học và thoạt nhìn chúng có thể giống như mâu thuẫn với nhau
Tài liệu liên quan: "Making Impossible States Impossible" của Richard Feldman
Thảo luận trước đó:
Đã chuyển cho Crowdstrike
Gợi nhớ đến một bình luận của ai đó vào thời kỳ bùng nổ XML giữa những năm 2000: nhiều tổ chức chọn XML vì XML cung cấp sẵn parser
Dù việc viết parser không hề khó và còn khá thú vị, tôi vẫn không hiểu vì sao mọi người lại không muốn tự viết parser
Tò mò không biết điều này có đi ngược với quan điểm cho rằng từ khóa "required" của Protocol Buffers là một sai lầm lớn hay không
Có lẽ tốt nhất là vừa có khả năng parse linh hoạt, không kiểm chứng, vừa có khả năng parse đã được kiểm chứng