1 điểm bởi GN⁺ 2025-10-05 | 1 bình luận | Chia sẻ qua WhatsApp
  • 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ữ AdaRust
  • 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 RustModula-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::FileBufReader
  • 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

 
GN⁺ 2025-10-05
Ý kiến Hacker News
  • Thật đáng tiếc khi Ada có rất nhiều ý tưởng rất hay nhưng phần lớn chỉ được dùng trong các lĩnh vực mà an toàn là tối quan trọng. Đặc biệt, tính năng giới hạn miền giá trị của kiểu số rất hữu ích để ngăn ngừa một số lỗi nhất định. Spark Ada vừa dễ học vừa dễ áp dụng để phát triển phần mềm tuân thủ SIL 4 (mức tiêu chuẩn an toàn phần mềm nghiêm ngặt nhất). Trong vài thập kỷ qua, ngành phần mềm đã lao theo hướng “tăng trưởng trước, độ ổn định để sau”, nhưng giờ có cảm giác đang dần quay lại với việc phát triển phần mềm an toàn. Mong rằng những bài học về an toàn đã tích lũy bấy lâu có thể dẫn tới các ngôn ngữ tốt hơn. Thực tế là nhiều ý tưởng hay thường bị ẩn trong các ngôn ngữ thiểu số rồi dần biến mất
    • Làm phần mềm đủ lâu sẽ thấy chuyện “phát minh lại bánh xe” xảy ra rất nhiều. Ada và Rust đều giống nhau ở chỗ theo đuổi tính an toàn, nhưng định nghĩa và phạm vi áp dụng của chúng khác nhau. Rust theo đuổi một dạng an toàn quan trọng rất tập trung và rất quyết liệt, còn Ada có định nghĩa rộng hơn và cụ thể hơn về an toàn. Khi tôi học Ada vào đầu những năm 90, lời phê bình phổ biến nhất là ngôn ngữ quá lớn và phức tạp nên làm chậm tốc độ phát triển (thời đó một compiler Ada 83 được chứng nhận có giá khoảng 20.000 USD/người theo giá trị ngày nay). Nhưng thời thế đã đổi, và mọi người đều công nhận rằng một ngôn ngữ lớn, phức tạp như Rust là cần thiết cho lập trình đồng thời an toàn trong thực tế
    • Nim cũng hỗ trợ subrange, lấy cảm hứng từ Ada và Modula, để giới hạn miền giá trị của kiểu
      type
        Age = range[0..200]
      
      let ageWorks = 200.Age
      let ageFails = 201.Age
      
      Khi biên dịch sẽ báo lỗi rằng 201 không thể chuyển sang kiểu Age
      Liên kết giải thích về Nim Subranges
    • Ada (theo GNAT) hỗ trợ phân tích đơn vị vật lý/kích thước ngay ở thời điểm biên dịch (tức kiểm tra đơn vị). Trong thực tế kỹ thuật, điều này cực kỳ hữu ích, nên thật khó hiểu vì sao các ngôn ngữ khác chỉ cung cấp tính năng quan trọng như vậy thông qua thư viện bên thứ ba
      Tài liệu liên quan
    • Trong C++ cũng có thể dễ dàng tự tạo kiểu số bị giới hạn miền giá trị bằng code (dù không có trong thư viện chuẩn, nhưng tự triển khai rất dễ). Một số kiểm tra an toàn có thể thực hiện ở thời điểm biên dịch chứ không phải lúc chạy. Tôi mong mọi ngôn ngữ đều hỗ trợ chuẩn những tính năng như vậy
    • Điều tôi thấy tiếc nhất ở Ada là cách tiếp cận rất rõ ràng đối với lập trình hướng đối tượng (OOP). Hầu hết các ngôn ngữ nhét các khái niệm OOP vào một khối gọi là “class”, còn Ada cho phép chọn áp dụng riêng rẽ message passing, dynamic dispatch, subtyping, generic, v.v. Tôi rất thích cách các tính năng này kết hợp với nhau một cách đẹp đẽ
  • Tác giả nhắc đến những khác biệt như Ada có đặc tả chính thức còn Rust thì không, nhưng từ góc nhìn người dùng, điều quan trọng hơn là mức độ chấp nhận/hệ sinh thái của ngôn ngữ (tooling, thư viện, cộng đồng). Ada đã thành công trong các lĩnh vực như hàng không vũ trụ/an toàn và phù hợp cho AOC hay công việc low-level trên hệ nhúng, nhưng trong các dự án thực tế (hệ phân tán, thành phần hệ điều hành, v.v.), những yếu tố như định dạng dữ liệu, giao thức, hỗ trợ IDE, khả năng cộng tác với đồng nghiệp mới chiếm tỷ trọng lớn. Cuối cùng, khi chọn ngôn ngữ ban đầu thì chính những yếu tố môi trường này mới mang tính quyết định
    • Gần đây Rust cũng đã nhận được một tài liệu đặc tả do Ferrocene đóng góp, tham khảo phong cách đặc tả của Ada. Tài liệu này được công khai nên có thể xem
      Đặc tả Rust
    • Cả Rust lẫn Ada đều còn yếu nếu xét theo nghĩa nghiêm ngặt của “đặc tả hình thức” (tài liệu có thể được chứng minh bằng máy). Ngay cả Spark Ada cũng dựa trên các giả định ngữ nghĩa ngôn ngữ, và ngay cả như vậy cũng chưa hoàn toàn chính thức theo cách máy có thể đọc được
    • Các nhà phát triển phần mềm điều khiển máy bay có lẽ cũng sẽ trả lời rằng: “Nếu đó là việc không quan trọng trong môi trường thực tế ngoài đời thì đúng là quy trình của chúng tôi hơi quá tay.” Thực tế, trong các lĩnh vực an toàn trọng yếu, những ngôn ngữ và quy trình nghiêm ngặt như Ada mới là tiêu chuẩn
  • Tôi thấy ấn tượng ở chỗ dù Ada có thể kém Rust về một số tính năng liên quan đến kiểu, nhưng về độ dễ đọc của code thì nhiều khi Ada lại tốt hơn. Bài so sánh không nhắc đến tốc độ compiler; chuyện Ada bị xem là ngôn ngữ phức tạp có lẽ là câu chuyện của quá khứ, và nếu so với Rust ngày nay thì chưa chắc còn đúng. Đọc bài này xong tôi thực sự muốn thử làm một dự án bằng Ada
    • Tôi tò mò “nhược điểm liên quan đến kiểu” ở đây chính xác là gì. Theo kinh nghiệm của tôi, hệ thống kiểu của Ada cực kỳ giàu khả năng biểu đạt. Có kiểu miền giá trị do người dùng định nghĩa, mảng có thể được đánh chỉ số bằng enum tùy ý, định nghĩa toán tử theo kiểu, thêm các chức năng phụ trợ cho kiểu như kiểm tra lúc biên dịch/lúc chạy, precondition, postcondition, v.v. Còn có discriminated record, structural representation clause, v.v. Đây không phải nhược điểm mà là những tính năng cực mạnh
  • Tôi muốn nói về sự khác biệt về chuỗi giữa Ada và Rust. Vì Ada được thiết kế từ đầu những năm 1980, “string” của nó là mảng ký tự (char), nên có thể đánh chỉ số như một mảng byte rất dễ dàng. Rust thì ngay từ đầu đã được thiết kế với ý thức về Unicode, nên chuỗi của Rust là UTF-8 encoded, tức là “text” đúng nghĩa. Vì vậy Ada có thể truy cập ngẫu nhiên như mảng, còn ở Rust do khái niệm chuỗi khác nên cũng có thể chọn chuyển sang mảng byte đơn thuần
    • Chuỗi Unicode tích hợp sẵn của Ada thường là mảng UTF-32. Khác với Rust, Ada không cung cấp trực tiếp literal UTF-8 mà phải chuyển đổi từ mảng 8/16/32 bit
    • Chuỗi trong Rust cũng có thể được index. Tuy nhiên, Rust không coi chuỗi như mảng thông thường mà chủ yếu dùng sub-string slice. Nếu cắt để index vào giữa một ký tự thì sẽ panic (trường hợp vi phạm ranh giới của giá trị mã hóa Unicode). Với những bài như AoC chỉ dùng ASCII thì hợp lý nhất là dùng byte slice với [u8] hoặc phương thức str::as_bytes
  • Tôi thấy lạ khi tác giả nói Rust “không hỗ trợ lập trình đồng thời (concurrent) như một tính năng mặc định”. Rust có tính năng thread tích hợp trong ngôn ngữ, và thực ra còn dễ dùng hơn async. Chỉ khi cần số lượng thread rất lớn đến mức đụng giới hạn tài nguyên thì mới thành vấn đề; với đa số phần mềm, thread built-in là đủ
    • (Tôi không dùng Rust nên hỏi thật lòng) Tôi muốn biết việc xử lý hủy/tạm dừng trong thread và async của Rust khác nhau thế nào, và nó khác gì so với async ở các ngôn ngữ khác. Trong C++, Python, C#, việc quản lý hủy trong async tốt hơn thread rất nhiều. Tôi nghe nói ở Rust, do không xử lý việc hủy/tạm dừng bằng exception nên việc này lại khó hơn; rất muốn biết trải nghiệm thực tế trong công việc. Cũng muốn nghe xem Ada xử lý chuyện này ra sao
    • Tôi tò mò đâu là ranh giới mà scheduler kiểu work-stealing như Tokio thực sự nhanh hơn việc chỉ chạy nhiều thread đơn thuần. Tương tự như mảng đơn giản (ví dụ: VecMap) khi số phần tử ít thì nhanh, nhưng vượt qua một ngưỡng nào đó thì cấu trúc dữ liệu khác hiệu quả hơn. Tôi muốn biết từ điểm nào thì work-stealing bắt đầu có lợi trong thực tế
    • Lý do chính để dùng Async trong thực tế là vì crate bên thứ ba mà bạn dùng là Async. (Ví dụ: Reqwest cần Tokrio.) Nếu trong phát triển ứng dụng mức cao mà cứ nhất quyết chỉ dùng non-Async thì sớm muộn cũng gặp giới hạn
    • Trong các môi trường mà nền tảng hỗ trợ thread yếu (WASM, hệ nhúng, v.v.) thì Async lại phù hợp hơn. Kịch bản hàng trăm nghìn người cùng vào một blog là không thực tế, nên việc viện dẫn các trường hợp như vậy để nhấn mạnh sự cần thiết của Async có vẻ hơi phóng đại
  • Thật thú vị khi biết Ada cũng có compiler mã nguồn mở. Trước đây tôi cứ nghĩ toàn là compiler độc quyền nên chưa từng quan tâm tới Ada; giờ chắc phải xem lại
    • Compiler GNAT đã ra mắt hơn 30 năm, và từng có hiểu lầm rằng do không có GPL runtime exception nên kết quả biên dịch cũng phải là GPL, nhưng hiện nay vấn đề đó đã được giải quyết
    • GNAT được xây dựng dựa trên GCC từ những năm 90, và ở một số trường đại học người ta từng trực tiếp dùng GNAT trong các môn thiên về thực hành như lập trình thời gian thực. Tôi cũng từng có trải nghiệm định dùng Ada để dạy nhập môn lập trình rồi nhanh chóng chuyển sang Pascal, C++
  • Trong lĩnh vực in 3D, một dự án gần đây khiến tôi chú ý là bo điều khiển máy in và firmware tên Prunt. Họ phát triển firmware bằng Ada, một lựa chọn khá lạ nhưng về mặt ý tưởng thì lại rất hợp
    Trang chủ Prunt
    Prunt GitHub
  • Ở cuối Case Study 2 có câu “nếu client cần biết SIDE_LENGTH thì hãy thêm một hàm trả về nó”, nhưng thay vì hàm thì dùng khai báo hằng như pub const SIDE_LENGTH: usize = ROW_LENGTH; sẽ trực tiếp hơn
  • Tôi không đồng ý với nhận định rằng cả hai ngôn ngữ đều khuyến khích lập trình xoay quanh stack. Ada trái lại còn tích cực khuyến khích cách phân bổ tĩnh
  • Tôi ngạc nhiên khi việc chỉ số mảng trong Ada có thể là kiểu tùy ý lại được giới thiệu như một ưu điểm lớn. Gần như mọi ngôn ngữ đều có dictionary (hash map) trong thư viện chuẩn, và Rust còn cung cấp hai loại
    • Điều đang nói ở đây là mảng được tích hợp ở cấp ngôn ngữ. Ví dụ, trong Ada nếu chỉ số của mảng “eggs” được khai báo là kiểu BirdSpecies thì eggs[Robin], eggs[Seagull] là hợp lệ nhưng eggs[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 khai Index<BirdSpecies>) và khi đó eggs[Robin] thì được còn eggs[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
    • Ada cũng có hỗ trợ sẵn hash map và set. Chuẩn liên quan đến Ada containers (xem mục A.18). Việc kiểu chỉ số mảng có thể dùng các miền “giá trị liên tiếp” điển hình (ví dụ: 0~N-1) là một ưu điểm lớn trong các tình huống cần dense map hoặc truy cập bộ nhớ liên tục, vì nhanh hơn dictionary rất nhiều và còn tốt cho cache
    • Việc giới hạn kiểu chỉ số mảng (subtype) trong Ada là một khái niệm hoàn toàn khác về mặt cấu trúc so với dictionary. Ngôn ngữ cho phép hạn chế cả tập giá trị của chỉ số mảng ở cấp độ ngôn ngữ