So sánh Ada và Rust qua việc giải bài toán AoC
(github.com/johnperry-math)- So sánh những khác biệt và đặc trưng nảy sinh khi giải các bài Advent of Code bằng hai ngôn ngữ Ada và Rust
- Phân tích sự khác nhau trong thiết kế ngôn ngữ và cách viết chương trình thực tế của hai ngôn ngữ cùng lấy an toàn và độ tin cậy làm trọng tâm
- Sự khác biệt hiện rõ ở nhiều góc độ như thư viện chuẩn của từng ngôn ngữ, tính năng có sẵn hay không, chênh lệch hiệu năng, phong cách xử lý lỗi, v.v.
- Giải thích các trường hợp cụ thể khi viết và vận hành thực tế thông qua ví dụ mã về tính mô-đun, generic, vòng lặp, xử lý lỗi, v.v.
- Sự khác biệt trong trải nghiệm phát triển nổi bật ở cách định kiểu tĩnh, xử lý mảng và giao diện xử lý lỗi
Giới thiệu và mục đích
- Trong quá trình giải Advent of Code (sau đây gọi là AoC), ban đầu chỉ dùng Ada, nhưng từ năm 2023 tác giả cũng viết lời giải bằng Rust và Modula-2, từ đó có cơ hội so sánh trực tiếp
- Khi chuyển các lời giải vốn tập trung vào Ada sang Rust, tác giả cảm nhận rõ sự khác biệt về cấu trúc và cách tiếp cận riêng của hai ngôn ngữ
- Mục tiêu là làm rõ những khác biệt trong sử dụng thực tế xét từ góc nhìn độ an toàn mã, độ tin cậy và thiết kế ngôn ngữ
Phiên bản ngôn ngữ dùng để so sánh
- Ada 2022 (tham chiếu thêm một số quy tắc của Spark 2014 khi cần)
- Rust 2021 (các so sánh chính dựa trên Rust 1.81.0)
Các tính năng bị loại trừ và tiêu chí so sánh
- Những tính năng tiêu biểu nhất của mỗi ngôn ngữ (= killer feature) chỉ được nhắc ngắn gọn dưới dạng bình luận trong bài
- Cũng có một số tính năng không được đề cập do kinh nghiệm cá nhân và nhu cầu thực tế của từng lời giải
- Cố gắng loại bỏ tối đa ý kiến chủ quan và tập trung vào các đặc điểm chính
Nền tảng và góc nhìn của tác giả
- Với cả Ada và Rust, tác giả đều là người dùng không phải bản ngữ, có nền tảng từ các ngôn ngữ thập niên 1980 như C/C++, Pascal, Modula-2
- Vì vậy, phong cách mã có thể khác với lối viết hiện đại hoặc thành ngữ phổ biến
- Cách cài đặt có thể chưa tối ưu, và tùy tình huống bài toán đôi khi tác giả chọn lời giải trực quan hoặc không theo thông lệ
Vị thế của Ada và Rust
- Ada vẫn là ngôn ngữ phát triển hệ thống/nhúng rất an toàn và có độ tin cậy cao, đồng thời coi trọng tính dễ đọc của mã
- Rust nổi bật về an toàn bộ nhớ và lập trình hệ thống, nhiều năm liền được xướng tên là “ngôn ngữ được yêu thích nhất” trong khảo sát nhà phát triển của Stack Overflow
- Ada là ngôn ngữ bậc cao đa dụng, cung cấp phổ tính năng chuyên cho việc đọc hiểu và bảo trì
- Rust hướng đến phát triển chương trình hệ thống mức thấp, thiết lập văn hóa lập trình an toàn dựa trên quản lý bộ nhớ tường minh và các kiểu lỗi/tùy chọn
So sánh độ an toàn và đặc điểm cấu trúc
-
Ada
- Chuẩn ISO (đặc tả chặt chẽ)
- Dễ khai báo kiểu phù hợp với đặc tính bài toán (phạm vi, số chữ số, v.v.)
- Chỉ số mảng không nhất thiết phải là số
- Có đặc tả Spark nghiêm ngặt hơn
-
Rust
- Đặc tả dựa trên tài liệu chính thức (Reference) và trình biên dịch
- Khai báo kiểu phụ thuộc vào kiểu máy (ví dụ: f64, u32)
- Việc lập chỉ số mảng về bản chất chỉ phù hợp tự nhiên với kiểu số
Tóm tắt chính từ bảng tính năng/tích hợp sẵn
- Có sự khác nhau ở các mặt như hỗ trợ kiểm tra phạm vi mảng, container generic, đồng thời, vòng lặp có nhãn, pattern matching
- Ada xử lý lỗi dựa trên Exception (ngoại lệ), trong khi Rust dùng cách trả về qua kiểu Result/Option
- Rust nổi bật khác biệt ở hỗ trợ macro, pattern matching, tính thuần hàm
- Ada hỗ trợ thiết kế theo hợp đồng và Spark hỗ trợ xác minh DBC (Design By Contract) tại thời điểm biên dịch
- Về an toàn bộ nhớ, Rust và Spark có cơ chế cưỡng chế mạnh hơn, còn Ada cho phép sử dụng con trỏ Null
So sánh hiệu năng và thời gian chạy
- Rust nhìn chung có thời gian chạy nhanh nhưng biên dịch chậm, còn Ada thì ngược lại, biên dịch nhanh hơn và thời gian chạy có thể chậm hơn đôi chút tùy mức kiểm tra xác minh
- Theo kết quả benchmark, ở bài day24, Rust gặp tràn số do giới hạn của kiểu f64, trong khi Ada có thể chỉ định kiểu bậc cao như digits 18, nhờ đó tự động chọn kiểu máy phù hợp và tránh tràn số, cho hiệu năng tốt
- Rust phải dùng f128 chưa ổn định hoặc thư viện ngoài, còn Ada có thể chiếm ưu thế chỉ bằng cách khai báo kiểu phù hợp với đặc tả của trình biên dịch
Xử lý tệp và xử lý lỗi (Nghiên cứu tình huống 1)
Xử lý tệp trong Ada
- Mặc định dùng Ada.Text_IO
- Có thể mở tệp tường minh, đọc theo từng dòng, xử lý theo phạm vi mong muốn hoặc theo vị trí dòng, tương đối trực quan
- Khi lỗi xảy ra, lỗi được xử lý bằng ngoại lệ thay vì thông báo rõ ràng, và khả năng phát sinh lỗi không lộ ra trong chữ ký hàm
Xử lý tệp trong Rust
- Sử dụng std::fs::File và BufReader
- Khi mở tệp, hàm trả về kiểu Result, nên khả năng xảy ra lỗi được thể hiện rõ ràng
- Không hỗ trợ truy cập trực tiếp theo chỉ số ký tự, bắt buộc phải xử lý bằng Iterator
- Các công cụ hàm/lặp như map, filter, collect, sum đóng vai trò trung tâm, cùng nhiều macro khác nhau (ví dụ: include_str!)
- Việc khai báo lỗi tường minh trong kiểu trả về giúp bảo đảm tính rõ ràng của lan truyền lỗi ở cấp độ hàm
Tính mô-đun và generic (Nghiên cứu tình huống 2)
Tính mô-đun trong Ada
- Dựa trên package, tách biệt rõ giữa đặc tả (giao diện) và phần cài đặt
- Để tăng cường mô-đun hóa, có thể kết hợp subpackage và cú pháp use/rename để điều chỉnh độ dễ đọc
- generic của package cho phép tổng quát hóa kiểu/hằng/toàn bộ subpackage
Tính mô-đun trong Rust
- Tổ chức mô-đun theo hệ mod/crate, việc phân tách đặc tả và cài đặt được công cụ sinh tài liệu tự động hóa
- Có chỉ định truy cập pub/private, cấp quyền truy cập theo kiểu khai báo
- Kết hợp use/as để import/đổi tên
- Hỗ trợ kiểm thử tích hợp sẵn cho phép khai báo mô-đun test trực tiếp trong mã, build và chạy tự động
Generic
- Ada chỉ hỗ trợ generic ở cấp package/procedure (không hỗ trợ riêng cho bản thân kiểu)
- Rust có thể áp dụng generic ngay trên kiểu (theo hướng template)
- Ada có thể biểu đạt rõ các thuộc tính bổ sung như miền giá trị của kiểu bằng range type, subtype, còn Rust tận dụng hằng số ở cấp instance
So sánh kiểu liệt kê (Nghiên cứu tình huống 3)
- Ada hỗ trợ khai báo ngắn gọn, đồng thời tự động hỗ trợ kiểu rời rạc, có thứ tự và sử dụng trong vòng lặp/chỉ mục
- enum của Rust có khai báo tương tự, nhưng cách tiếp cận cho pattern matching hay lặp cần tường minh hơn
Kết luận
- Ada cung cấp khả năng kiểm soát nghiêm ngặt hơn ở các mặt như kiểu đặc tả bậc cao, khả năng xác minh và kiểm tra khi chạy
- Rust vượt trội hơn cả về tiện lợi phát triển lẫn an toàn ở các mặt như phong cách lập trình hàm, lập trình macro, xử lý lỗi được trình biên dịch hỗ trợ
- Trong giải quyết bài toán thực tế, Ada có thế mạnh về tương thích với mã cũ và bảo trì, còn Rust có lợi thế ở hệ sinh thái công cụ phát triển hiện đại cùng hỗ trợ an toàn/song song
1 bình luận
Ý kiến Hacker News
Liên kết giải thích về Nim Subranges
Tài liệu liên quan
Đặc tả Rust
str::as_bytesTrang chủ Prunt
Prunt GitHub
Bình luận HN liên quan
pub const SIDE_LENGTH: usize = ROW_LENGTH;sẽ trực tiếp hơneggs[Robin],eggs[Seagull]là hợp lệ nhưngeggs[5]thì không được phép. Trong Rust cũng có thể tự tạo cấu trúc dữ liệu mong muốn (ví dụ triển khaiIndex<BirdSpecies>) và khi đóeggs[Robin]thì được còneggs[5]sẽ báo lỗi. Tuy nhiên Rust không hỗ trợ trực tiếp điều này như một “mảng” ở cấp ngôn ngữ. Khi Ada cho phép “kiểu do người dùng định nghĩa có thể được khai báo là kiểu số nguyên giới hạn miền giá trị”, thì kiểu indexing như vậy mới thực sự phát huy giá trị. Rust hiện vẫn chưa thể tạo kiểu integer giới hạn miền giá trị bằng kiểu thuần do người dùng định nghĩa (chỉ có những kiểu như NonZeroI16 ở mức nội bộ). Nếu Rust hỗ trợ được đến mức đó thì thật tuyệt