Cốt lõi của Rust
(jyn.dev)- Rust là một ngôn ngữ có nhiều khái niệm đan xen chặt chẽ với nhau, nên ngay cả để hiểu một chương trình cơ bản cũng phải học đồng thời nhiều yếu tố
- Hàm, generic, enum, pattern matching, trait, tham chiếu, ownership,
Send/Sync,Iterator... đều là những yếu tố cốt lõi được thiết kế để tương tác với nhau - So với JavaScript, JS cho phép viết code chỉ với việc hiểu một phần khái niệm, nhưng với Rust, chỉ khi hiểu được ngữ cảnh của toàn bộ ngôn ngữ thì mới có thể viết ra code thực sự có ý nghĩa
- Sự phức tạp này của Rust làm tăng rào cản học tập, nhưng đồng thời mang lại tính an toàn và nhất quán, và có ảnh hưởng lớn tới cách thiết kế code
- Cấu trúc ngôn ngữ như vậy chính là điều làm Rust trở nên đặc biệt, và tầm nhìn về “Rust nhỏ hơn” khiến ta nhìn lại triết lý ngôn ngữ được kết hợp một cách tinh vi
Sự khó khăn khi học Rust
- Dù Rust có rào cản gia nhập cao, nhiều người vẫn đã đóng góp vào việc cải thiện tài liệu, API và chẩn đoán lỗi
- Các khái niệm nền tảng bao gồm hàm như công dân hạng nhất, enum, pattern matching, generic, trait, tham chiếu, borrow checker, an toàn đồng thời, iterator
- Những khái niệm này phụ thuộc lẫn nhau và đan xen với nhau, nên khó học từng phần một cách tách biệt; ngay cả thư viện chuẩn cũng tận dụng hầu hết các tính năng đó
- Ngay cả để hiểu một đoạn mã Rust khoảng 20 dòng, cũng cần đồng thời nắm được nhiều yếu tố như mô hình hàm,
Resultvà xử lý lỗi, kiểu generic, enum, iterator...
So sánh Rust và JavaScript
- Khi viết cùng một chương trình theo dõi thay đổi tệp bằng Rust và JS, Rust cho thấy nhiều khái niệm ngôn ngữ đan cài với nhau
- Với JS, về cơ bản chỉ cần hiểu hàm và xử lý null là đã có thể viết được code chạy ổn
- Điều này không đơn thuần có nghĩa là Rust khó hơn, mà cho thấy Rust là một thiết kế đòi hỏi sự hiểu biết có cấu trúc về toàn bộ ngôn ngữ
Thiết kế liên kết chặt chẽ của Rust
- Cốt lõi của Rust là sự kết hợp của các tính năng được thiết kế một cách hữu cơ
- Enum sẽ bất tiện nếu không có pattern matching, và pattern matching cũng bị hạn chế nếu không có enum
ResultvàIteratorkhông thể được triển khai nếu không có generic- Các khái niệm
Send/Syncvà ràng buộc củaprintlnchỉ có thể được biểu đạt an toàn khi có trait - Borrow checker bảo đảm an toàn
Send/Syncthông qua việc phân tích capture của closure
- Sự gắn kết lẫn nhau này khiến Rust trở thành một hệ thống ngôn ngữ tích hợp, chứ không chỉ là tập hợp đơn thuần của các tính năng
Tầm nhìn về Rust nhỏ hơn
- Năm 2019, without.boats đã nhắc đến “Smaller Rust” khi bàn về khả năng của một Rust nhỏ gọn và tinh lọc hơn
- Ngày nay Rust đã lớn hơn rất nhiều, nhưng khái niệm Rust nhỏ hơn vẫn nhắc lại bản chất của một thiết kế ngôn ngữ được ăn khớp tinh vi
- Sức hấp dẫn của Rust nằm ở chỗ các yếu tố ngôn ngữ vừa độc lập vừa khi kết hợp lại tạo ra khả năng biểu đạt mạnh mẽ và tính an toàn
Kết luận
- Rust khó học, nhưng tính nhất quán và tích hợp của các khái niệm đan xen với nhau lại là một thế mạnh lớn
- Nhờ cấu trúc đó, Rust khiến lập trình viên không chỉ viết code, mà còn hình thành cách tư duy đồng thời cân nhắc cả an toàn lẫn hiệu năng
- Bản chất của Rust nằm ở một “ngôn ngữ cốt lõi nhỏ gọn và tinh vi”, và đó vẫn là một triết lý quan trọng ngay cả trong Rust đã được mở rộng như ngày nay
1 bình luận
Ý kiến trên Hacker News
fs.watchghi rõ rằng trong callback phải kiểm tra trường hợpfilenamecó thể lànull. Với Rust, thực tế này sẽ được phản ánh trong hệ thống kiểu và buộc bạn phải xử lý, còn trong JS thì rất dễ viết đại cho xong. Tài liệu liên quannullsẽ bị bắt buộc. Vì vậy đây là một ví dụ hay cho thấy TS là một bước tương đối nhẹ nhàng để đưa JS tiến gần hơn tới tính đúng đắn kiểu Rustfor path in pathsphải làfor (const path of paths). JS sẽ báo lỗi ngay nếu thiếu ngoặc, nhưng khác biệt giữainvàoflàinsẽ lặp qua chỉ số của iterable chứ không phải giá trị, nên thực tế chỉ số sẽ bị chuyển thành string rồi đi vào đối số đầu tiên củafs.watch. Thậm chí TypeScript cũng có thể không bắt được lỗi nàykindđến từ đâu. Trongconsole.log("${kind} ${filename}")thì đúng ra phải làeventType(một string), chứ không phảikindprintlncủa Rust chỉ in được các kiểu có triển khai traitDisplayhoặcDebug. Vì thếPathkhông thể in trực tiếp. Không phải mọi OS đều lưu path theo UTF-8, trong khi kiểu string của Rust thì toàn bộ là UTF-8. Nghĩa là việc inPathcó thể gây mất mát dữ liệu.Pathtrả về một kiểu có triển khaiDisplaythông qua phương thứcdisplay. Rust đã đưa điều này vào hệ thống kiểu, nhưng trong JS/TS thì rất khó thể hiện chuyện string nội bộ là UTF-16, và path không phải Unicode thì phải dùng trực tiếpTextEncoder/TextDecodermới xử lý đúng được. Từ kinh nghiệm trước đây, nếu server gửi text bằng Shift_JIS mà đọc bằngresponse.text()thì ở runtime chỉ ra chuỗi rỗng. Nếu không quen với vấn đề encoding thì có thể mất vài ngày chỉ để debug chuyện này. Ngoài ra ví dụ JS có bug và lỗi cú pháp mà code Rust không có (trong vòng lặp cầnfor-ofthay vìfor-in). Cũng khó nói ví dụ này chỉ dùng "hàm hạng nhất"; bạn vẫn cần hiểu iterator như Rust, lại còn dùng CommonJS. Rồi còn phải học thêmasync/await,Promisesvà top-levelawait, mà top-levelawaitthì chỉ mới được hỗ trợ gần đây ở một số runtime, bao gồm node. Vẫn còn những JS engine không hỗ trợ, ví dụ Hermes của React NativeĐó là lý do tôi vẫn tiếp tục dùng Rust. Đây chỉ là một ví dụ, nhưng những vấn đề vặt vãnh và cạm bẫy kiểu này luôn có mặt đầy rẫy ở các ngôn ngữ khác. Từng cái riêng lẻ có thể không xảy ra, nhưng cộng dồn trong toàn bộ vòng đời chương trình thì chỗ này chỗ kia cứ liên tục xuất hiện bug kỳ quặc và bạn phải không ngừng truy tìm. Trong Rust thì chuyện đó không xảy ra. Hệ thống kiểu chặn trước một lượng trường hợp vô lý đến mức phi thường. Thực tế là sau khi tôi phát hành phần mềm đã hoàn thiện tính năng bằng Rust, về sau chỉ thỉnh thoảng thêm tính năng mới, còn công việc sửa bug thông thường gần như biến mất. Dĩ nhiên ở đâu cũng có thể có bug logic, nhưng Rust chặn tận gốc những vấn đề xuất phát từ sự lệch kiểu/cấu trúc ngớ ngẩn như trong các ngôn ngữ khác, nên trải nghiệm về năng suất và bảo trì hoàn toàn khác
Cá nhân tôi cảm thấy không có nhiều lập trình viên JS/TS thực sự hiểu tử tế về thenable/Promise và async-await. Tôi từng thấy cả những thứ như thế này:
Họ bọc nguyên wrapper kiểu callback vào Promise rồi lại dùng nó bên trong một hàm async. Mỗi lần gặp kiểu này tôi thấy rất đau lòng. Tôi thực sự thấy code như vậy ở khắp nơi. Ngoài ra, nếu tính cả import module và
async import(), transpile, code splitting, v.v. thì đúng là rất phức tạp-Zscriptđể nghiên cứu mà bị phân tâm sang đây. Nó đã được triển khai từ 2023 và có cả các issue mở trông gần như sắp hoàn thành. Tôi cũng thấy trong kho lưu trữ ZomboDB việc xử lý pipeline build bằng rust, dù chưa hiểu toàn bộ bối cảnh. Tôi muốn nhấn mạnh rằng cargo frontmatter cực kỳ hữu ích cho tính di động của script. Chỉ cần chia sẻ một file duy nhất, rồi có thể lấy dependency và dùng ngay mà không cần cài đặt/khởi tạo bổ sung như Python hay Node.js#!/some/paththì shell chỉ việc chuyển toàn bộ file đó vàostdincủa lệnh được chỉ định để thực thiCopytrait, reborrowing, deref coercion, tự độnginto_itertrong vòng lặp, tự động gọi drop khi scope kết thúc (cái này cũng có thể gọi trực tiếp hoặc để compiler báo lỗi), mặc định:Sizedtrong trait bound, lifetime elision, match ergonomic và nhiều yếu tố tự động hóa/tiện lợi khác, thì bạn có thể có một Rust đơn giản theo kiểu rất cơ học. Nhưng một ngôn ngữ như vậy sẽ cực kỳ khó chịu để dùng hằng ngày. Trớ trêu là nhiều yếu tố trên thực ra lại được thiết kế cho người mới bắt đầucoherence), mà là trở thành một ngôn ngữ hữu dụng trong công nghiệpDeref..into()và traitFromxử lý chuyển đổi kiểu quá âm thầm. Trong standard library cũng có nhiều hàm "tiện lợi" kiểu này. Cuối cùng kiểu thực của đối tượng trở nên mơ hồ, và khó lần ra mối liên hệ giữa lời gọi hàm với phần cài đặt của nó (dĩ nhiên IDE có giúp phần nào)implicit return) che khuất luồng chương trình và dễ gây sai sót. Toán tử dấu hỏi cũng không hợp gu tôi lắmconst, đôi khi còn giúp bạn đỡ phải mất công gỡ bỏ các thói quen xấu học từ những ngôn ngữ cũ về saumem, nên nếu muốn hiểu chắc cấu trúc interface thì tốt nhất hãy bắt đầu từ std::mem