Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung)
85

Tin ngành

Trải nghiệm viết lại thư viện Rust bằng Zig: Góc nhìn từ một lập trình viên kỳ cựu

(giờ Việt Nam)

Tóm tắt AI

Một lập trình viên với 7 năm kinh nghiệm Rust đã thử sức viết lại dự án jsonpath-rust sang ngôn ngữ Zig để tuân thủ chuẩn RFC 9535, mang đến những góc nhìn thú vị về sự khác biệt giữa hai ngôn ngữ hệ thống này.

Bản dịch AI

Giới thiệu

Tôi đã dành 7 năm qua làm lập trình viên Rust, chủ yếu làm việc với các dự án mã nguồn mở, và tôi tự tin rằng mình đã xây dựng được cảm quan vững chắc về ngôn ngữ này cũng như hệ sinh thái của nó. Tôi có xu hướng thiên về khía cạnh lập trình hàm (functional) của Rust như các hàm gọn gàng, kiểu dữ liệu biểu cảm, đại loại như vậy. Nhưng tôi luôn tò mò về các ngôn ngữ khác, và Zig đã nằm trong tầm ngắm của tôi một thời gian như một ứng viên kế nhiệm C: cấp thấp hơn, nhẹ hơn và đang dần khẳng định vị thế trong số các ngôn ngữ được giới chuyên môn coi trọng. Tôi từng dành thời gian làm việc với C hồi đầu sự nghiệp, nên việc so sánh giữa hai ngôn ngữ này luôn là điều thú vị.

Một lưu ý nhỏ cần nói trước: kinh nghiệm của tôi với Zig bắt đầu từ chính dự án này. Một số quan sát có thể trông khá ngây ngô và hiển nhiên đối với những người làm việc với Zig hàng ngày, và một số quyết định tôi đưa ra gần như chắc chắn không phải là tối ưu; chúng được định hình bởi những thói quen từ Rust hơn là sự thấu hiểu sâu sắc về thành ngữ (idiom) của Zig. Điều đó không sao cả, ai cũng phải bắt đầu từ đâu đó, và trong lúc này, tôi đang dựa vào bất kỳ trực giác đa ngôn ngữ nào mà mình đã tích lũy được qua nhiều năm, dù tốt hay xấu.

Để đảm bảo tính công bằng khi so sánh, tôi quyết định triển khai lại một thứ mà tôi đã từng xây dựng bằng Rust. Đó không phải là một dự án đồ chơi, nhưng cũng không phải là một dự án quá đồ sộ, và lý tưởng nhất là thứ mà cộng đồng có thể thực sự sử dụng. Tôi đã chọn JSONPath: một ngôn ngữ truy vấn cho JSON, được quy định trong RFC 9535. Phiên bản Rust đã tồn tại (jsonpath-rust), và mục tiêu là mang điều tương tự sang Zig: zig-jsonpath.

Hỗ trợ IDE

Điều đầu tiên khiến tôi bất ngờ — và thành thật mà nói, ai mà ngờ được đây lại là phần đáng nhớ — chính là hỗ trợ IDE, hoặc gần như là không có gì cả. Tôi đã quen dùng RustRover cho Rust và các biến thể JetBrains khác cho những ngôn ngữ khác, còn Zig, khi so sánh, chỉ cung cấp rất ít tính năng ngoài việc tô sáng cú pháp và tự động hoàn thành cơ bản. Điều này không hẳn là đáng ngạc nhiên, nhưng nó buộc tôi phải quay lại những điều cơ bản: học cách làm việc với ngôn ngữ chủ yếu từ dòng lệnh (command line). Những gì bắt đầu như một bất lợi lại trở thành một trong những phần thú vị nhất của trải nghiệm. Hóa ra tôi đã quên mất việc dựa vào các công cụ CLI thuần túy có thể đơn giản đến mức nào.

Bài học thực tế đầu tiên ở đây là build.zig, thứ xử lý việc này với sự dễ dàng đáng kinh ngạc. Cuối cùng tôi đã chọn thiết lập này:

Một khi bạn đã chấp nhận các điều khoản, việc làm việc với nó thực sự rất mới mẻ.

Tôi phải cảm ơn Zig, theo một cách gián tiếp, vì đã khởi đầu một chuỗi phản ứng lớn hơn, đó là việc tôi chuyển từ một IDE đầy đủ sang thiết lập helix + alacritty + zellij.

Cấu trúc phẳng

Với Rust và hầu hết các ngôn ngữ khác, tôi luôn dành khá nhiều thời gian (đắn đo qua lại) để cố gắng tìm sự cân bằng phù hợp giữa kích thước tệp và độ sâu của thư mục. Bạn được tự do phân mảnh các tệp và mở rộng hệ thống phân cấp thư mục sâu bao nhiêu tùy thích. Hóa ra Zig cũng ổn với điều này, nhưng bằng cách nào đó lại không thực sự khuyến khích nó (giống như C, điều không có gì ngạc nhiên đối với một ngôn ngữ hệ thống cấp thấp). Bạn có thể lồng các tệp và thư mục nếu muốn, nhưng làm như vậy sẽ gây ra một chút khó khăn khi import, và câu hỏi thực sự trở thành: tại sao phải bận tâm? Bạn thực sự đạt được gì về khả năng đọc khi chia nhỏ mọi thứ vào nhiều tệp và thư mục hơn? Về lý thuyết là khả năng đọc tốt hơn. Trên thực tế, khi bạn gộp các thứ liên quan vào một tệp lớn hơn, bạn chỉ cần cắt nhỏ và điều hướng từng phần, và việc có mọi thứ ở một nơi mang lại lợi ích thực sự. Hầu hết, Zig thúc đẩy bạn hướng tới cấu trúc phẳng. Nếu một thứ gì đó cần một thành phần đi kèm cho một model, tôi chỉ cần tạo một tệp model_<companion> bên cạnh nó và tiếp tục công việc.

Tôi không nghĩ cách này có thể mở rộng cho các dự án lớn, nghĩa là đến một lúc nào đó bạn cần một hệ thống phân cấp thực sự, nhưng ngưỡng cần thiết cho việc đó hóa ra lại cao hơn nhiều so với tôi mong đợi trong Zig. Trong Rust, tôi có xu hướng tìm đến cấu trúc thư mục từ sớm, gần như theo mặc định. Trong Zig, tôi cứ trì hoãn nó, và đến cuối dự án này, tôi hoàn toàn không cần đến nó.

Sự tương phản đó hữu ích vượt xa phạm vi của Zig, vì nó khiến tôi cân nhắc lại, ngay cả với các ngôn ngữ khác, liệu tôi đang tổ chức các tệp vì dự án thực sự cần hay chỉ vì thói quen. Đó cũng là một cách khá trung thực để đánh giá quy mô thực sự của một dự án: nếu bạn không thể cưỡng lại việc tạo thư mục ngay từ ngày đầu tiên, có lẽ nó nhỏ hơn bạn tưởng.

Đây là sự khác biệt thực tế, đặt cạnh nhau:

Rust (src/):

Zig (src/):

Kiểm thử (Tests)

Tạm gác lại bộ kiểm thử tuân thủ rfc9535 và chỉ tập trung thuần túy vào bản thân ngôn ngữ:

Trong Rust, tôi có xu hướng gắn bó với hai phương pháp kiểm thử:

Tôi mong đợi sự phân chia tương tự từ Zig. Trên lý thuyết, nó trông có vẻ giống nhau: bạn có thể viết các bài kiểm thử trực tiếp bên trong cùng một tệp. Vấn đề, ít nhất là đối với tôi, là sự rườm rà. Với cấu trúc phẳng mà tôi đã chọn, tôi chỉ còn hai lựa chọn: hoặc một tệp model_test riêng biệt cho mỗi model, hoặc các bài kiểm thử được chèn trực tiếp vào chính tệp model đó. Cả hai cách tiếp cận đều gây lộn xộn: hoặc là các tệp riêng lẻ hoặc là toàn bộ thư mục chính.

Tôi đã chọn phương án thứ hai, nghĩa là phải cấu hình nó một cách rõ ràng trong build.zig. Tuy nhiên, một khi đã thiết lập xong, nó hoạt động tốt và vẫn giữ được sự gọn gàng.

Tóm lại: việc viết và quản lý các bài kiểm thử đối với tôi dễ dàng hơn trong Rust. Nhưng trong trường hợp của Zig, phần lớn sự khó khăn đó là đặc thù của ngôn ngữ, nó xuất phát từ việc quản lý bộ nhớ thủ công của Zig thay vì bản thân cơ sở hạ tầng kiểm thử.

Không có mô hình lập trình hàm

Rust về mặt kỹ thuật là một ngôn ngữ mệnh lệnh (imperative), nhưng nó vay mượn rất nhiều từ các khái niệm lập trình hàm: iterator chi phí bằng không, đánh giá lười (lazy evaluation), ADT, khớp mẫu (pattern matching), kiểu monadic, trait, closure, v.v. Sau khi dành thời gian với Haskell và Erlang, tôi đã trở nên khá thiên về phong cách lập trình hàm, và điều đó thể hiện rõ trong thư viện này. Nó dựa rất nhiều vào các thành ngữ FP:

Tôi biết ngay từ đầu rằng mình sẽ không thể mang tất cả những điều này sang Zig, nhưng tôi hy vọng ít nhất mình có thể giữ lại các khái niệm cốt lõi. Trên thực tế, nơi Rust dựa vào tính bất biến (immutability) và các combinator, Zig lại thúc đẩy tôi hướng tới việc thay đổi dữ liệu tại chỗ (in-place mutation) và các mô hình tự nhiên nhất đối với thế giới mệnh lệnh.

Nơi mà hai ngôn ngữ này vẫn gần gũi: kiểu tổng (sum types).

Thuần túy và trực tiếp trong Rust:

Duck-typed trong Zig:

Đệ quy cũng được giữ vững ở cả hai phía.

Rust:

Zig:

Nhưng ngôn ngữ này nhanh chóng buộc bạn phải tách rời khỏi phong cách lập trình hàm, chủ yếu là vì bạn đang làm việc trực tiếp với các bộ cấp phát bộ nhớ (allocators), và một cách tiếp cận lập trình hàm thuần túy thực sự đồng nghĩa với việc liên tục tạo ra các cấu trúc mới. Điều đó hoặc là tốn kém về bộ nhớ, hoặc tốn kém về công sức quản lý thủ công cần thiết để tránh điều đó.

Sự khác biệt cốt lõi nằm ở Mutation (thay đổi trạng thái) so với immutable monad.

Rust thực hiện một phép biến đổi monadic trực tiếp:

Zig chuyển sang mutation:

Reduce so với Fork.

Rust:

Zig:

Combinator so với vòng lặp.

Rust:

Đọc bài gốc

Bài viết được AI dịch và tổng hợp tự động từ Hacker News Nổi bật (buzzing.cc bản dịch tiếng Trung). Liên kết bài gốc ở phía trên. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.