- Khoảng 3 năm trước, khi tự tay viết một bytecode VM và garbage collector bằng Zig và unsafe Rust, tính công thái học thân thiện với con người của Zig chiếm ưu thế; nhưng khi bước vào thời đại coding agent, lợi thế đó gần như trở nên vô nghĩa
- Mức tăng năng suất 1,5~5 lần cho lập trình viên mà các tính năng chủ chốt của Zig mang lại bị lấn át bởi mức tăng năng suất 100 lần mà coding agent trên nền Rust mang lại
- Các tính năng cốt lõi của Zig như allocator interface, số nguyên với độ rộng bit tùy ý, packed struct, comptime đều là những tính năng tỏa sáng khi con người trực tiếp viết mã
- Hệ thống kiểu của Rust hiệu quả hơn trong việc ngăn lỗi của agent ngay từ thời điểm biên dịch thông qua bounded polymorphism và việc cưỡng chế các bất biến
- Trong bối cảnh lượng mã do agent tạo ra tăng gấp 100 lần, đảm bảo an toàn bộ nhớ của Rust là lợi thế mang tính quyết định so với Zig
Thay đổi cốt lõi
- Zig từng có lợi thế lớn về tính công thái học của mã unsafe, nhưng khi tỷ trọng con người trực tiếp viết mã giảm đi, giá trị thực tế của lợi thế đó cũng giảm theo
- Mức cải thiện năng suất 1,5~5 lần cho lập trình viên con người mà các tính năng của Zig đem lại bị che khuất bởi mức tăng 100 lần khi dùng coding agent với Rust
- Phần lớn các tính năng tiêu biểu của Zig giúp tăng sự tiện lợi khi con người tự tay viết mã, nhưng với coding agent thì khác biệt đó không quá quan trọng
- Khoảng 3 năm trước, khi viết bytecode VM và garbage collector bằng Zig và unsafe Rust, tác giả cảm thấy trải nghiệm viết mã unsafe bên Zig tốt hơn
- Tính đến năm 2026, Zig vẫn là một ngôn ngữ tốt, nhưng Rust đã trở thành ngôn ngữ được ưa chuộng hơn và cũng hợp với coding agent hơn
Giao diện allocator của Zig
- Giao diện allocator của Zig giúp dễ dàng áp dụng các allocator chuyên biệt như arena hoặc stack fallback để tối ưu cho những đường đi mã cụ thể
- Khi đọc một dòng đầu vào của người dùng, độ dài đầu vào về lý thuyết là không giới hạn nên cần heap allocator, nhưng trên thực tế phần lớn đầu vào chỉ là từ khóa tìm kiếm hoặc đường dẫn ngắn, nhỏ hơn rất nhiều so với 1KB
std.heap.stackFallback(256, heap_allocator) cho phép đặt một buffer kích thước cố định trên stack, và chỉ chuyển sang heap khi dữ liệu tràn quá mức đó, nên trong trường hợp thông thường có thể xử lý mà không cần cấp phát heap
- Trước đây trong Rust không có tính năng tương ứng với giao diện
Allocator của Zig, nên nếu cần Vec<T> dùng allocator tùy chỉnh thì phải sao chép phần triển khai của thư viện chuẩn rồi sửa lại
- Mã nguồn collection của Bumpalo là dạng fork các collection chuẩn rồi nối chúng với bump allocator
- Trong Rust nightly đã từng có
Allocator trait trong một thời gian, và hiện nay nó có vẻ đã đạt đến mức đủ tốt
Allocator của Rust dựa trên trait nên dùng static dispatch, khác với allocator của Zig vốn dựa trên vtable
- Rust không có quy ước ở tầm cộng đồng về việc thiết kế cấu trúc dữ liệu dựa trên tham số allocator như Zig, nhưng khi AI có thể dễ dàng sao chép và chỉnh sửa mã thì hạn chế này trở nên kém quan trọng hơn
Số nguyên độ rộng bit tùy ý và packed struct
- Số nguyên với độ rộng bit tùy ý và
packed struct của Zig giúp việc tối ưu CPU cache theo kiểu thiết kế hướng dữ liệu, tagged pointer, NaN boxing, bitflags trở nên dễ dàng hơn
- Khi dùng Obj-C API cùng với Metal qua Objective-C runtime C API,
id có thể là tagged pointer chứ không phải con trỏ đối tượng heap đã được căn chỉnh
- Nếu truyền
NSNumber dạng tagged vào đoạn mã giả định có căn chỉnh, UB có thể xảy ra, nên cần một cách kiểm tra rẻ xem đó là “con trỏ heap hay immediate được gắn tag”
- Trong cách bố trí tagged pointer Objective-C đã được đơn giản hóa, 1 bit thấp nhất biểu thị “không phải con trỏ heap”, 3 bit tiếp theo nhận diện class slot, và 60 bit còn lại là payload
- Zig cho phép dùng
enum(u3) và packed struct để biểu diễn trực tiếp layout bit bằng kiểu, chẳng hạn class: TaggedClass, payload: u60
- Trong Zig, có thể dùng
@bitCast để chuyển qua lại giữa u64 thô và ObjcTaggedPointer, rồi trong is_ns_number kiểm tra is_tagged và class .ns_number
- Mã tương ứng trong Rust đặt các hằng như
TAG_MASK, CLASS_MASK, CLASS_SHIFT, PAYLOAD_SHIFT bên trong ObjcTaggedPointer(u64), dùng phép OR khi tạo và áp dụng mask khi truy cập
- Trong Rust, class slot không phải là kiểu thực sự mà là các hằng
u64, và cách viết thủ công này kém công thái học hơn so với Zig
- Trong Rust, tốt hơn nên dùng các crate như bitfield hoặc bitflags, nhưng cả hai đều phụ thuộc vào proc macro và vẫn không mang lại cảm giác tốt bằng
packed struct của Zig
- Khi có coding agent, việc khó chịu vì phải tự tay viết loại mã này giảm đi rất nhiều
Sự thay đổi về giá trị của comptime
- comptime của Zig là tính năng hào nhoáng nhất, và ngoài một vài ngôn ngữ kiểu phụ thuộc khá khó nhằn, gần như không có ngôn ngữ nào cung cấp khả năng đánh giá tại thời điểm biên dịch tốt như Zig
- Trong sử dụng thực tế, tác giả không còn nhớ comptime nhiều như trước, và khoảng 95% cách dùng của nó là để tạo các cấu trúc dữ liệu generic có tham số kiểu
- Mẫu như
fn ArrayList(comptime T: type) type, nhận vào một kiểu rồi trả về một kiểu struct có items: []T, capacity: usize, allocator: Allocator, là ví dụ tiêu biểu
- Hệ thống kiểu của Rust thay thế được phần lớn generic comptime kiểu Zig, đồng thời có thể cưỡng chế nhiều điều kiện bất biến hơn
- Với khoảng 5% trường hợp còn lại, việc thiếu comptime gây bất tiện, và phương án thay thế đáng tin cậy hầu như chỉ có codegen
- Khi phát triển game, nếu muốn hardcode dữ liệu hitbox geometry được sinh ra từ công cụ vào cấu trúc dữ liệu, thì trong Rust phải để Claude viết một script sinh ra file Rust
- Dù vậy, trên thực tế nhu cầu đánh giá ở thời điểm biên dịch không xuất hiện thường xuyên đến thế
Ưu điểm của hệ thống kiểu Rust
- Hệ thống kiểu của Rust được đánh giá là sự đánh đổi đáng giá hơn comptime của Zig, đặc biệt mạnh ở mảng traits/typeclasses cho bounded polymorphism
- Nếu cố hiện thực mức bounded polymorphism tương đương trong Zig thì sẽ rất khó
- Hệ thống kiểu Rust có thể cưỡng chế nhiều điều kiện bất biến (invariant) hơn, giúp ngăn các lỗi mà coding agent thường mắc phải
- Trong mã game, tác giả dùng crate euclid để ngăn sự nhầm lẫn giữa các không gian tọa độ, một vấn đề phổ biến trong lập trình đồ họa
- Nếu tạo các kiểu chuyên biệt cho từng không gian tọa độ như
Point<Screen> hoặc Point<World>, thì việc vô tình trộn tọa độ thế giới với tọa độ màn hình sẽ bị chặn ngay ở bước biên dịch
- Nếu có các kiểu riêng như
WorldPoint, WorldVector, ScreenPoint, thì phép cộng giữa point và vector trong cùng một không gian sẽ được cho phép
- Thông qua
Translation2D::<f32, WorldSpace, ScreenSpace>, có thể chuyển đổi một cách tường minh từ không gian thế giới sang không gian màn hình
- Ngược lại, mã như
let bad: ScreenPoint = player;, tức gán trực tiếp WorldPoint vào ScreenPoint, sẽ không được chấp nhận
Hiệu ứng của việc phải xử lý ít vấn đề bộ nhớ hơn
- Nếu coding agent khiến việc viết ra lượng mã nhiều hơn gấp 100 lần trở nên khả thi, thì lượng vấn đề bộ nhớ cần rà soát trong mã Zig cũng tăng lên gấp 100 lần
- Nếu không có kiểm chứng hình thức, diện tích của không gian tìm kiếm cần phải xem xét để tìm lỗi sẽ lớn hơn rất nhiều
- Trong bối cảnh hiện tại, khi lượng mã được sinh ra ngày càng lớn, Rust trở nên hấp dẫn hơn
- Sự đánh đổi truyền thống của Rust là nó làm giảm năng suất của lập trình viên khi chưa quen borrow checker, nhưng khi có coding agent thì tầm quan trọng của nhược điểm này giảm đi đáng kể
- Ngay cả khi dùng
unsafe trong Rust, vẫn có thể để coding agent chạy các công cụ như miri để kiểm tra xem có phát sinh UB hay không, có vi phạm các quy tắc aliasing của Rust hay không
Kết luận
- Zig vẫn là một ngôn ngữ đáng nhớ và là một ngôn ngữ tốt
- Trong cách làm việc của năm 2026, Rust được ưa chuộng hơn, và mức độ tương thích với coding agent của Rust cũng tốt hơn
1 bình luận
Ý kiến trên Lobste.rs
Trưởng nhóm cũ của tôi có một quan điểm khá mạnh rằng code copy-paste không phải lúc nào cũng xấu
Theo bản năng thì điều đó nghe có vẻ sai hoặc gây tranh cãi vì nguyên tắc DRY, nhưng anh ấy là người rất thực dụng và chủ yếu áp dụng nguyên tắc này cho các codebase kiểm thử lớn
Lập luận là thay vì cố ép tạo ra một giao diện dùng chung thật thông minh, một codebase đơn giản hơn nhưng lớn hơn và nhiều lặp lại hơn có thể lại dễ bảo trì hơn
Dạo này khi dùng LLM tôi lại quay về cùng suy nghĩ đó, và giờ còn áp dụng cho cả những phần phần mềm quan trọng hơn
Việc sinh mã thì nhanh, và LLM dường như cũng có khả năng làm đúng tốt hơn với codebase đơn giản nhưng nhiều lặp lại
Nếu cố giảm trùng lặp bằng cách đưa quá nhiều trừu tượng vào test thì test sẽ khó hiểu hơn và có nguy cơ sai lệch tinh vi
Tệ hơn nữa là nếu tái sử dụng các trừu tượng của chính phần mã đang được kiểm thử, thì test cũng có thể sai theo cùng cách với phần mã đó
Thêm nữa, khác với mã ứng dụng, test về cơ bản được “ghép” gần như miễn phí
Miễn là bạn không phá hỏng nghiêm trọng test harness, thì việc thêm hoặc xóa test tùy ý cũng không ảnh hưởng đến các test khác, và không có ma sát tích hợp, nên lại bớt đi một lý do phải tránh trùng lặp
Trong test, tôi từng thấy điều này được diễn đạt bằng DAMP: “Descriptive and Meaningful Phrases”, tức là một nguyên tắc nhấn mạnh khả năng đọc hơn là tính độc nhất
Nguyên tắc này có thể tạo ra sự trùng lặp theo kiểu lặp lại mã tương tự, nhưng nó làm cho test trông hiển nhiên là đúng hơn
https://testing.googleblog.com/2019/12/…
Cộng đồng Go cũng có câu tương tự: “một bản sao nhỏ còn tốt hơn một phụ thuộc nhỏ” https://go-proverbs.github.io/
Tôi rất biết ơn vì anh ấy đã chia sẻ live coding
Nếu nhớ không nhầm thì khi bắt đầu làm gì đó, anh ấy thường tìm đoạn mã giống nhất với thứ mình định làm, sao chép nguyên khối rồi sửa từ đó
Tôi đã kiểu như “không ngồi xuống suy nghĩ thật lâu xem trừu tượng chung của hai cái là gì à?”, nhưng anh ấy cứ copy-paste rồi tiến lên, và năng suất hơn tôi rất nhiều
Nghĩ lại thì thời điểm AI viết lại Zig → Rust của Bun khá thú vị https://xcancel.com/jarredsumner/status/2053063524826620129#m
Trong số đó, 75 PR được nói là sẽ không thể biên dịch nếu dùng một ngôn ngữ có destructor, move semantics và borrow checker
Tức là cứ ba PR được triển khai thì có một PR kiểu “quên giải phóng ở đường lỗi”
Trong 108 PR đó, khoảng 88 là ở phía Zig, còn khoảng 14 ở phía C++ thì phần lớn là các loại tồn dư vẫn còn trong bất kỳ ngôn ngữ nào, như vòng tham chiếu và race condition trong đồng thời với GC
Vì vậy khác biệt Zig→Rust là có thật, và bug phía Zig đúng là loại có thể được sửa chính xác bằng destructor và ownership, còn phía C++ thì đã gần chạm đáy rồi
Nếu không có các bảo đảm mạnh hơn ở thời điểm biên dịch thì chuyện này sẽ tiếp tục là trò mèo vờn chuột
Đề xuất là thay vì cứ tiếp tục sửa riêng lẻ nhóm bug lớn nhất, hãy loại bỏ nó ở mức cấu trúc
– bun/docs/rust-rewrite-plan.md at claude/phase-a-port · oven-sh/bun · GitHub
Đoạn “trong 5% trường hợp còn lại thì không có comptime sẽ rất khổ, và cách duy nhất để reliably đạt được kết quả tương đương là sinh mã” không làm rõ tác giả muốn nói gì
Vì họ chẳng nói gì về macro thủ tục cả
Làm cho tử tế thì hơi phiền, nhưng nó làm được rất nhiều việc
Tôi cũng nghĩ việc sinh mã bị mang tiếng xấu một cách hơi oan
Tôi đã giải quyết khá nhiều vấn đề khó chịu bằng sinh mã với script
build.rs, và nó chạy tốtTất nhiên cũng có thể sau này tôi sẽ hối hận
Luận điểm cốt lõi của bài có vẻ đại khái là thế này
Rust đúng là một ngôn ngữ tốt, nhưng thế này vẫn hơi quá
Trông như quảng cáo cho coding agent
Đây là nội dung từ bài được liên kết của cùng tác giả: trong lập trình đồ họa, một lỗi cực kỳ phổ biến là nhầm lẫn không gian tọa độ, và hệ thống kiểu đủ mạnh để biểu diễn bằng kiểu không gian tọa độ nào và phép biến đổi nào là hợp lệ
Điều tương tự cũng đúng với tiền tệ, khoảng cách và trọng lượng theo hệ SI và hệ Anh-Mỹ, chuỗi đã được kiểm chứng so với chuỗi do người dùng cung cấp, cũng như giá trị bí mật
Ngoài ra nếu quản lý tốt thì còn có thể dùng kiểu trạng thái để chặn những trạng thái bất khả thi hoặc không sound
Nhưng cá nhân tôi, tính năng tôi muốn nhất ở Rust là loại bỏ hoàn toàn data race
Ngôn ngữ managed cũng có data race
Với kiểu “cứ dùng Go đi”, trong Go mọi thứ đều có thể thay đổi qua tham chiếu giữa các thread, lại còn kèm theo trò uốn dẻo với slice
Ngay cả JavaScript, vốn từng là một ngôn ngữ hoàn toàn đồ chơi và thuộc phe thuần, an toàn hơn, thì mọi
awaitcũng là một race tiềm tàngChưa nói đến sự tà ác của kiểu everything-is-an-EventEmitter
Nên đúng vậy. Giá mà chỉ có GC thôi thì… 🤫
Có cảm giác như đang che bớt phần cốt lõi
Coding agent cũng xử lý Python và JavaScript rất tốt
Việc nó có tốt hơn Rust hay không thì thuộc vùng cảm tính chủ quan, nhưng dù vậy tôi cũng sẽ không chọn những ngôn ngữ đó cho nhiều công việc
Tôi tò mò vấn đề nằm ở chỗ tính năng của Zig thay đổi thường xuyên, hay chỉ đơn giản vì nó là ngôn ngữ mới hơn nên dữ liệu huấn luyện AI bị nhiễu loạn
Tôi thấy Zig khó viết hơn Rust nhưng lại dễ đọc hơn
Trong thời đại AI, lượng mã được đọc nhiều hơn lượng mã được viết, nên tôi nghiêng về phía Zig hơn
Với lượng mã đang được sinh ra hiện nay thì việc nói Rust hấp dẫn hơn mới chỉ là bước đầu
Máy tính càng viết nhiều mã, thì các ngôn ngữ hình thức sẽ càng có lợi thế
Trông như một giai đoạn khác của cuộc tranh luận về kiểu động
Kiểu như “kiểu động thì dễ hơn cho con người, vậy tại sao lại phải khai báo cùng một thứ ba lần chỉ để phục vụ máy?”
Kiểu, lifetime… ngoài ra còn những gì khác sẽ khiến máy dễ viết và dễ tiêu thụ hơn
Tôi tò mò không biết ngôn ngữ mà máy tính tương lai dùng để viết mã sẽ khiến con người khó code trực tiếp đến mức nào