Kiểu tĩnh và cái xẻng
(carefully.understood.systems)- Từ những năm 2000 đến đầu những năm 2010, mức độ phổ biến của kiểu tĩnh giảm xuống rồi lại tăng trở lại vào nửa sau thập niên 2010, và điều này được lý giải bằng sự cải thiện về chất lượng của hệ thống kiểu tĩnh
- Hệ thống kiểu động buộc con người phải tự phán đoán trạng thái và nội dung của biến cùng các trường, và được ví như đào đất bằng tay không khi máy tính không giúp cũng không cản trở
- Các hệ thống kiểu tĩnh trong quá khứ như Java thời kỳ đầu hay C++98 thậm chí không giúp phân biệt con trỏ nullable và non-nullable, đồng thời bắt người dùng lặp đi lặp lại tên kiểu, nên được ví như cái xẻng bằng giấy
- Các hệ thống kiểu hiện đại như TypeScript, Haskell, MyPy, Swift, Rust hỗ trợ tốt hơn cho việc kiểm tra lỗi chương trình và biểu diễn trạng thái thông qua xử lý null, sum type·union type và suy luận kiểu
- Khi các tính năng như tự động hoàn thành tên phương thức trong IDE trở nên phổ biến, thông tin đưa vào hệ thống kiểu tĩnh không chỉ phục vụ kiểm tra lỗi mà còn mang lại lợi ích về năng suất
Luận điểm cốt lõi
- Quan điểm được đưa ra là mức độ phổ biến của kiểu tĩnh tăng trở lại không chỉ đơn thuần là một trào lưu, mà vì chất lượng của hệ thống kiểu tĩnh đủ thực dụng để dùng rộng rãi đã được cải thiện
- Bài viết dùng phép so sánh rằng nếu có một cái xẻng tốt thì đào đất bằng xẻng sẽ tốt hơn dùng tay không, nhưng nếu chỉ có cái xẻng làm bằng giấy thì dùng tay không lại tốt hơn
- Hệ thống kiểu động đòi hỏi con người phải tự suy nghĩ xem các biến và trường trong chương trình đang có trạng thái và nội dung gì, còn máy tính không hỗ trợ cho sự phán đoán đó
- Một hệ thống kiểu tĩnh tệ có thể tạo ra gánh nặng lớn hơn giá trị nó mang lại, và điều này được ví với việc đào đất bằng cái xẻng giấy
Giới hạn của các hệ thống kiểu tĩnh trong quá khứ
- Các hệ thống kiểu tĩnh được dùng phổ biến trong thập niên 90 và đầu những năm 2000 như Java thời kỳ đầu hay C++98 thậm chí còn không hỗ trợ tốt cho việc phân biệt đơn giản giữa con trỏ nullable và con trỏ non-nullable
- Các hệ thống kiểu tĩnh trong quá khứ được mô tả là có cấu trúc không có sum type mà chỉ có product type
- Các hệ thống kiểu tĩnh trong quá khứ buộc người lập trình phải tự viết tên kiểu bằng tay ở khắp nơi
- Đoạn mã như
BufferedReader bufferedReader = new BufferedReader(new FileReader(filename));được mô tả như một thảm họa nho nhỏ
Những cải tiến của hệ thống kiểu hiện đại
- Các hệ thống kiểu hiện đại như TypeScript, Haskell, MyPy, Swift, Rust cung cấp cách để phân biệt giữa kiểu nullable và kiểu non-nullable
- Haskell dùng
Maybe t, TypeScript dùngT | null, Swift dùngT?, Rust dùngOptional<T>làm ví dụ, cho thấy hệ thống kiểu có thể chỉ ra vị trí cần kiểm tra null và nơi đang bị bỏ sót - Bài viết giải thích rằng trên thực tế, người ta gần như không còn thấy lỗi null pointer ở runtime nữa
- Các hệ thống kiểu hiện đại cung cấp ít nhất một trong hai thứ là sum type hoặc union type, cho phép thực hành "Make invalid states unrepresentable"
- Cách tiếp cận này giúp biểu diễn các đối tượng dạng state machine sao cho mỗi trường chỉ tồn tại khi ở trạng thái liên quan
- Các hệ thống kiểu hiện đại cung cấp suy luận kiểu, nên nếu trình biên dịch có thể hiểu
let x = 5;là một số thì không cần phải viếtlet x: number = 5;
Tính năng IDE và kết luận
- Khi các tính năng IDE như tự động hoàn thành tên phương thức trở nên phổ biến, tính hữu ích của hệ thống kiểu tĩnh càng tăng lên
- Trong thập niên 90, Intellisense là tính năng chủ lực của Visual Studio, nhưng đến thập niên 2020 thì các tính năng tương tự đã có trong gần như mọi IDE và trình soạn thảo
- Thông tin đưa vào hệ thống kiểu tĩnh không chỉ tạo ra giá trị trong việc kiểm tra lỗi chương trình mà còn mang lại thêm lợi ích về năng suất
- Một hệ thống kiểu động tốt vẫn tốt hơn một hệ thống kiểu tĩnh tệ, nhưng hiện nay chúng ta có thể dùng các hệ thống kiểu tĩnh tốt hơn rất nhiều so với trước đây
1 bình luận
Ý kiến trên Lobste.rs
Bài này hay, nhưng tôi không hoàn toàn đồng ý. Dù hệ thống kiểu tĩnh đầu những năm 2000 không quá xuất sắc, tôi vẫn cho rằng nó tốt hơn rất nhiều so với việc hoàn toàn không có kiểu tĩnh
Không có kiểu tổng đóng, nhưng vẫn có thể mô hình hóa phần lớn bằng kiểu con; không có kiểu non-null, nhưng tham chiếu và kiểu không phải con trỏ của C++, cùng các kiểu nguyên thủy của Java, đã đảm nhiệm một phần việc đó. Trong Ruby hay JavaScript, mọi kiểu không chỉ đều có thể là null mà còn có thể bị đối xử như chuỗi, như số nguyên, hay như mọi kiểu khác trong chương trình, nên tình hình còn tệ hơn
Tôi nghĩ lý do lớn khiến xu hướng với kiểu tĩnh thay đổi là vì trong cơn bùng nổ mạng xã hội Web 2.0, lợi thế người đi trước quan trọng hơn tất cả. Dù có tích lũy nợ kỹ thuật với Ruby hay Python, phát hành nhanh và lặp nhanh vẫn tốt hơn là bị vượt mặt như Friendster hay Digg; còn nếu chậm, khi đó chỉ cần mua thêm máy chủ bằng nguồn vốn lãi suất thấp rất dễ kiếm
Sau đó, trong làn sóng bùng nổ di động, phần mềm phải chạy trên các thiết bị người dùng bị giới hạn và không thể kiểm soát, nên ứng dụng dùng kiểu động chậm thì đơn giản là chậm thật, còn lỗi kiểu thì cũng không thể phục hồi gọn gàng bằng một bộ xử lý phản hồi cấp cao nhất như trên máy chủ. Trong môi trường đó, độ an toàn và hiệu năng của kiểu tĩnh trở nên thuyết phục hơn nhiều
Hồi đầu những năm 2000 tôi cũng đồng ý, vì khi đó các hệ thống kiểu thường chỉ ép buộc những thuộc tính gần như không bao giờ sai, trong khi lại áp đặt các ràng buộc không giúp ích cho việc cấu trúc mã. Đặc biệt, cách kết hợp kiểu con với kế thừa triển khai rất thiếu linh hoạt
Tôi đã đổi ý sau khi dùng các hệ thống kiểu hiện đại hơn. Trong snmalloc, chúng tôi dùng hệ thống kiểu của C++ để cưỡng chế máy trạng thái quyền sở hữu bộ nhớ, còn ở các codebase khác thì kiểm tra hành vi tràn đúng của bộ đếm ring buffer. Cả hai đều là những thứ nếu sai thì rất khó debug và là nguyên nhân lỗi phổ biến, nhưng trình biên dịch đã thật sự báo lỗi với những đoạn mã mà tôi tưởng là đúng, nhờ đó chặn bug không lọt vào nhánh chính
Trong IDE, bấm
.rồi gõ vài ký tự tên phương thức và nhấn Enter ở ứng viên đúng giúp tiết kiệm 2 giây mỗi vài giây, và còn tiết kiệm 30 giây tìm định nghĩa lớp khi bạn không biết có những phương thức nào. Nguyên lý này cũng được diễn đạt khá rõ tại https://grugbrain.dev/#grug-on-type-systemsVì người ta viết các dòng gọi phương thức thường xuyên hơn nhiều so với việc khai báo kiểu tham số hàm, nên sự đánh đổi nghiêng áp đảo theo hướng bất lợi cho kiểu động. Điều thực sự có giá trị không phải là cho phép những đoạn mã ngớ ngẩn sẽ thất bại lúc runtime, mà là cho phép lược bỏ kiểu của biến cục bộ; và ngay từ đầu ngôn ngữ kiểu tĩnh đâu cần cấm điều đó
Những codebase hiếm hoi dùng hệ thống kiểu một cách nghiêm túc chất đầy từng trang mã chẳng nói lên điều gì, vậy mà vẫn ngập các câu lệnh điều kiện runtime; còn với Java, chương trình cũng thực sự chậm đi khi hệ phân cấp kiểu phình ra. Phần lớn codebase dùng kiểu lác đác nhưng lại thêm rất nhiều điều kiện runtime, nên về mức độ bao phủ kiểm thử cần thiết cũng chẳng tiết kiệm được bao nhiêu so với hệ thống kiểu động
Ngôn ngữ kiểu động thì không có phần thưởng tĩnh, nhưng lại ngắn gọn, dễ đọc, dễ review và dễ test hơn. Điều này đặc biệt đúng trong những môi trường như framework tiêm phụ thuộc cuối thập niên 90 đến đầu những năm 2000, nơi cứ mỗi lần thêm dịch vụ mới là phải sửa hàng loạt file XML. Người ta cũng có thể làm việc mà không cần IDE ngốn mất nửa dung lượng RAM
Giai đoạn đầu sự nghiệp của tôi đúng hệt như vậy, nên tôi hoàn toàn đồng ý với bài viết. Tỷ lệ chi phí/hiệu quả từ Java 1.4 tới Java 6 quá tệ đến mức tôi gần như từ bỏ ngôn ngữ kiểu tĩnh, và chỉ vài năm sau khi nghịch Haskell như một sở thích tôi mới nhận ra kiểu tĩnh cũng có thể có tỷ lệ chi phí/hiệu quả hợp lý, và vấn đề là ở Java. Bài luận “python is not java” cũng tái hiện rất rõ thời kỳ đen tối đó
Tôi nghi ngờ liệu sau khi kiểu tĩnh trở thành tinh thần thời đại, chúng ta có thực sự thấy được lợi ích về độ tin cậy của phần mềm hay không
Tôi từng nghĩ ưu điểm của kiểu tĩnh nằm nhiều hơn ở phản hồi phát triển tức thời và việc giảm các lỗi runtime nghiêm trọng, nhưng dù về lý thuyết những lỗi như vậy luôn có thể xảy ra thì trên thực tế có vẻ chúng không xảy ra thường đến thế
undefinedvànullđã giảm mạnhJunior và một số senior ban đầu hoài nghi rằng
@ts-ignoresẽ mọc lên khắp nơi, nhưng thực tế chỉ có khoảng ba chỗ, kể cả những chỗ phát sinh do kiểu bị hỏng của dependency. Trước đây khoảng mỗi tuần một lần app lại chết trên nhánh phát triển vì nhầm lẫn kiểu, chặn công việc của tôi; giờ thì tôi còn không nhớ lần cuối chuyện đó xảy ra là khi nàoChỉ riêng việc làm cho
tschài lòng cũng đã giúp giảm bug liên quan đến kiểu, kể cả trong những phần không phải do chính tôi viết. Ngược lại, các linter dạo này quá mức nhiệt tình, và tôi từng thấy việc cố làm hài lòng các công cụ như Sonar lại làm hỏng refactor thật sự. 95% cảnh báo là giả, 3% là lỗi của chính công cụ, và 2% hữu ích còn lại cũng không phải nguyên nhân thật sự của bug. Thay vì bỏ 1 tuần để chỉnh cho codebase khớp và bắt được 1 bug, tôi lại đưa thêm 2 bug khác vào trong quá trình đóViệc làm cho
tschài lòng trung bình tạo ra khoảng 2 lần sửa bug thuần và 1 lần hồi quy mỗi ngày, nhưng hồi quy thường chỉ ở mức hành vi sai chứ không phải crash toàn bộ nên mức độ nghiêm trọng thấp hơnThêm kiểm thử dựa trên thuộc tính vào đây thì trung bình mất 2~4 giờ và luôn lộ ra ít nhất một bug. Nếu code của bạn có thể kiểm thử theo kiểu này thì nên làm
Tôi dùng DeepSeek V4 Flash, một model giá rẻ, để tăng phạm vi test nhưng cẩn thận không tạo ra test rác, và nhờ đó sửa được khoảng 2~3 bug logic mỗi ngày, không có crash nào. Tuy vậy, bộ test chỉ vừa đủ ở mức còn có thể bảo trì được
Khi để junior dùng đại các dòng Sonnet và Opus 4.5, 4.6 để tạo test, các model chỉ tạo ra những test “ghi chép hành vi hiện tại”, nên hiệu quả sửa lỗi thấp, còn bộ test thì không thể bảo trì nổi nên cuối cùng phải bỏ
Kiểm thử dựa trên mô hình rất tốt trong việc bắt bug, nhưng thiết lập phức tạp, và việc dẫn nó đi đào sâu các góc cạnh thay vì đốt chu kỳ vào chức năng bề mặt thì cực kỳ phiền. Nếu có thứ như model-based fuzzer dựa trên profile thì sẽ rất thú vị
Tóm lại, trình kiểm tra kiểu bắt rất tốt các lỗi nghiêm trọng và nhiều kiểu nhầm lẫn, còn kiểm thử dựa trên thuộc tính thì xuất sắc. Kiểm thử thông thường cần rất nhiều kỷ luật nếu muốn nhận được lợi ích ổn định
Điều tôi khó đồng tình nhất ở đây là việc gộp TypeScript với một hệ thống kiểu tốt
awaitđã nhiều lần làm tôi dính đòn. Nhưng đúng là nó đã cải thiện tình hình một cách đáng kểThành thật mà nói, cuối cùng tôi cũng chấp nhận kiểu hóa cấu trúc, và tôi nghĩ nó sẽ có ảnh hưởng tích cực đến thiết kế ngôn ngữ trong tương lai
Lập luận này kém thuyết phục. Những ngôn ngữ lập trình tử tế với kiểu dữ liệu đại số và suy luận kiểu đã tồn tại từ giữa thập niên 90
Hệ thống kiểu của Java và C++ rất nghèo nàn, nhưng SML, OCaml, Haskell thì đã có rồi và cảm giác sử dụng cũng không khác quá nhiều so với ngày nay. Nếu mọi người không dùng những ngôn ngữ đó, thì đó là vấn đề về văn hóa, mức độ chấp nhận, và những nhu cầu không được diễn đạt thành lời, chứ không thể chỉ giải thích bằng việc “hệ thống kiểu khả dụng chưa đủ tốt”
Hoặc nếu lập luận là “hệ thống kiểu của các ngôn ngữ phổ biến khi đó tệ, còn hệ thống kiểu của các ngôn ngữ phổ biến ngày nay tốt hơn nên hệ thống kiểu trở nên phổ biến hơn”, thì điều đó nghe như một kiểu lập luận vòng tròn
Cũng có rất nhiều sắc thái trong sự khác biệt giữa các ngôn ngữ được thiết kế cùng với hệ thống kiểu và các ngôn ngữ vốn được thiết kế không có kiểu rồi sau này mới chắp thêm hệ thống kiểu vào
Dù vốn dĩ tôi thích kiểu động, tôi vẫn thấy bài này khá công bằng. Giờ tôi làm việc bằng C#, sở thích cá nhân thì dùng Lisp, và trước đây cũng từng dùng Python
Khi phải dùng Java 5, tôi hầu như luôn phải vật lộn với hệ thống kiểu, chủ yếu vì những quyết định tồi của tác giả thư viện. Sau khi chuyển sang C# vào khoảng năm 2010, hệ thống kiểu không còn chủ động gây hại nữa, nhưng phần lớn vẫn chỉ là dư thừa, và nó cũng không ngăn được null pointer exception, vốn là kiểu nhầm lẫn phổ biến nhất trong Python
Hệ thống kiểu của C# chỉ thực sự bắt đầu hữu ích vào khoảng năm 2020 khi có kiểu tham chiếu không thể null. Năm nay cũng sẽ có union type gốc, nhưng các thư viện union type có ép tính đầy đủ thì ít nhất đã khả thi từ năm 2016, và tôi bắt đầu dùng chúng từ năm 2020
Tôi vẫn nghĩ trào lưu còn đóng vai trò, nhưng một phần của nó không hề xấu. Những ngôn ngữ đang thịnh hành với hệ thống kiểu biểu đạt tốt hơn đã mang lại cải tiến cho cả những ngôn ngữ bình thường mà chúng ta dùng để kiếm tiền
Haskell và hệ thống kiểu của nó đã tồn tại từ những năm 2000. Khi đó nó chưa được dùng rộng rãi như bây giờ, nhưng rõ ràng là có tồn tại, nên lập luận này cần bổ sung phần đó
Cá nhân tôi cho rằng TypeScript là một yếu tố lớn khiến người dùng ngôn ngữ phổ thông trở nên quen thuộc hơn với các hệ thống kiểu tốt hơn. Ngoài chất lượng và sự hậu thuẫn từ Microsoft, nó còn có lợi thế là áp dụng cho JavaScript, và JavaScript thì cần kiểu hơn Python nhiều. Lý do là “Undefined is not a function.” và “The good parts.”
“Real World Haskell” xuất bản năm 2008, với mục tiêu làm cho Haskell trông hấp dẫn hơn đối với các lập trình viên phổ thông. Tôi không rõ nó đã giúp lan truyền tin tốt đó đến mức nào
Trong thế giới Java, Scala đã mang đến hệ thống kiểu ấn tượng từ năm 2004, còn .NET thì có F# vào năm 2005. Scala có lẽ đã thu hút được những người dùng nổi bật nhất như Twitter, nhưng nó không ở vị thế có thể hấp thụ một tỷ lệ lớn người dùng trên nền tảng đó như TypeScript, cũng không đủ hấp dẫn để lôi kéo hàng loạt người dùng từ ngôn ngữ khác như Rust hay Go
Ngay đoạn sau đó bài viết nhắc đến Haskell như một “hệ thống kiểu hiện đại”, nhưng vào cuối thập niên 90 và đầu những năm 2000, số người có kinh nghiệm với Haskell thực tế gần như bằng 0%, kể cả tính cả mức độ chỉ từng tự tay thử qua. Bài viết đang nói về cách mà đa số lập trình viên khi đó trải nghiệm các ngôn ngữ kiểu tĩnh, và vì sao phần đông trong số họ đã đồng loạt né tránh ngôn ngữ kiểu tĩnh
Ví dụ, để dùng
dunetrong OCaml, bạn phải hiểu fileopam, filedune, cú phápocaml module, và cú phápocaml. Các phần mở rộng trình biên dịch tùy chọn của Haskell cũng tạo cảm giác đáng sợ y như vậyĐiều này tương phản với
cargo, nơi bạn chỉ cần biếttomlvà Rust