Zig và "trait tự chế": Cách lập trình viên Việt có thể có đa hình mạnh mẽ mà không cần tính năng ngôn ngữ
Bài viết trên blog cá nhân của Axel Magnus phân tích cách áp dụng mô hình trait trong Zig thông qua comptime, lấy cảm hứng từ module của OCaml. Đây là hướng tiếp cận thú vị giúp các lập trình viên hệ thống đạt được trừu tượng hóa mạnh mẽ, hiệu năng cao mà vẫn giữ được triết lý đơn giản của Zig.

Zig và “trait tự chế”: đa hình mạnh mẽ mà không cần tính năng ngôn ngữ
Zig vốn nổi tiếng với triết lý đơn giản, không có trait hay typeclass như Rust hay Haskell. Nhưng một bài viết mới đây trên blog của Axel Magnus cho thấy hoàn toàn có thể “tự chế” trait bằng comptime, biến chúng thành những khối xây dựng linh hoạt cho lập trình hệ thống hiệu năng cao.
Bài viết gốc mang tên “We have traits at home” — một cách chơi chữ quen thuộc trên mạng, ám chỉ việc tự làm thứ gì đó ở nhà thay vì dùng sản phẩm có sẵn. Với Zig, “ở nhà” nghĩa là tận dụng sức mạnh của comptime để tạo ra các hợp đồng kiểu (type contract) ngay tại thời điểm biên dịch.
Bối cảnh: Zig giải quyết đa hình như thế nào?
Hiện nay, lập trình viên Zig có hai cách chính để đạt được đa hình:
- Interface động (Dynamic Interfaces): dùng con trỏ opaque, bảng hàm ảo (vtable) và điều phối động (dynamic dispatch) để có đa hình lúc chạy.
- Duck typing thời gian biên dịch (Comptime Duck Typing): dùng phản chiếu kiểu, câu lệnh
switchvà monomorphization để có đa hình tĩnh.
Mỗi cách đều có đánh đổi. Interface mang lại hợp đồng kiểu rõ ràng nhưng tạo rào cản cho trình biên dịch, dẫn đến chi phí hiệu năng do gián tiếp hóa. Duck typing bằng comptime thì nhanh nhưng dễ gây mơ hồ về hợp đồng, và đặc biệt nếu đến từ Rust hay Haskell, nhiều người sẽ thấy thiếu vắng một thứ tương đương với trait.
Trait theo phong cách OCaml
Tác giả đề xuất một quy ước mới: trait là một struct thời gian biên dịch chứa các hàm liên quan đến một kiểu dữ liệu cho trước. Ý tưởng này lấy cảm hứng trực tiếp từ module của OCaml — vốn cũng là các giá trị hạng nhất (first-class) được truyền tường minh vào hàm, chứ không được trình biên dịch tự suy diễn như trait của Rust.
Minh họa trait trong Zig
Ví dụ đơn giản với một trait Reducer cho phép rút gọn (reduce) một mảng:
fn Reducer(T: type) type {
return struct {
empty: T,
combine: fn (T, T) T,
fn reduce(self: @This(), xs: []const T) T {
var accum = self.empty;
for (xs) |x| accum = self.combine(accum, x);
return accum;
}
};
}
Từ đây, ta có thể định nghĩa các hiện thực (implementation) cho phép cộng và phép nhân trên i32, rồi dùng chúng chỉ với một dòng lệnh. Điểm mấu chốt: trình biên dịch biết chính xác cần gọi hàm nào, nên có thể inline và tối ưu hóa vòng lặp.
So sánh với các cách tiếp cận khác
Bài viết minh họa bằng phép biến đổi Möbius (a * x + b) / (c * x + d) — một công thức hữu ích cho cả số phức lẫn số hữu tỉ.
- Duck typing comptime: viết
a.mul(x).add(b)hoạt động với kiểu người dùng tự định nghĩa, nhưng không dùng được với kiểu nguyên thủy và buộc phải theo mô hình hướng đối tượng. - Interface với vtable: linh hoạt lúc chạy nhưng cồng kềnh, chậm, và không có gì ngăn việc trộn lẫn số phức với số hữu tỉ gây hành vi không xác định.
- Trait: gom các hàm số học vào một struct, truyền tường minh vào hàm — vừa rõ ràng hợp đồng, vừa giữ được monomorphization.
Với trait, việc mở rộng sang i32 hay f32 chỉ cần một hàm builtinNum sinh ra hiện thực phù hợp, xử lý khác nhau cho số nguyên (chia lấy phần nguyên) và số thực. Cách này đặc biệt hữu ích cho vòng lặp nóng (hot loop) hoặc các hàm thư viện không phụ thuộc vào cách bố trí dữ liệu.
Hạn chế cần lưu ý
Trait trong Zig phải được điều kiện hóa trên một kiểu đã biết trước. Không có cách nào nói “cho tôi một số bất kỳ, không cần biết kiểu cụ thể” như interface. Điều này khiến trait không tương thích tốt với kỹ thuật xóa kiểu (type erasure) — một hạn chế mà tác giả thừa nhận chưa tìm được cách khắc phục gọn gàng.
Trợ thủ và functor
Bài viết giới thiệu hai hàm tiện ích giúp giảm boilerplate:
impl: tự động trích xuất các khai báo từ một struct ẩn danh để lấp đầy một trait.getTrait: tìm trait được khai báo sẵn dưới dạngconsttrong struct — về cơ bản là “duck typing có kiểm soát”.
Đáng chú ý hơn là trait functor — hàm nhận một trait và trả về một trait mới, tương đương với module functor của OCaml. Đây là dạng curry hóa: áp dụng tham số kiểu trước, tham số giá trị sau. Nhờ đó, một hàm như Mobius có thể “đóng gói” trait Num và sinh ra phiên bản chuyên biệt cho từng kiểu.
Những trait hữu ích nên có sẵn
Tác giả chia sẻ những trait mình dùng thường xuyên:
Into/From: chuẩn hóa việc chuyển đổi giữa các kiểu.Index: truy cập theo khóa vào một container mà không cần biết đó là slice hay hash table.Iterator/Iterable: duyệt qua tập hợp mà không phụ thuộc vào cấu trúc dữ liệu cụ thể.
Lời cảnh báo quan trọng
“Trait là món ăn thỉnh thoảng, không phải món chính.”
Tác giả nhấn mạnh không nên chuyển toàn bộ mã nguồn sang dùng trait. Mỗi tham số comptime sinh ra một bản assembly riêng, và lồng ghép nhiều lớp có thể gây bùng nổ tổ hợp, làm phình kích thước binary, làm hỏng bộ nhớ đệm CPU và thậm chí chậm hơn cả dynamic dispatch.
Trait nên được dùng theo hướng “kéo” (pull-based): chỉ khi có một hàm thực sự cần trừu tượng hóa một chi tiết hiện thực, và đặc biệt trong các vòng lặp hiệu năng cao nơi monomorphization là bắt buộc.
Ý nghĩa với lập trình viên Việt Nam
Zig đang dần được cộng đồng lập trình viên hệ thống Việt Nam quan tâm nhờ hiệu năng gần với C, an toàn bộ nhớ tốt hơn và khả năng tích hợp với mã C cũ. Việc hiểu rõ các mẫu trừu tượng như trait tự chế giúp các nhóm phát triển có thêm lựa chọn khi xây dựng thư viện, đặc biệt trong các dự án xử lý tín hiệu, game engine hoặc hệ thống nhúng — nơi chi phí điều phối động là không thể chấp nhận.
Điều thú vị là trait ở đây chỉ là một quy ước lập trình, không phải tính năng ngôn ngữ. Nó không cần thư viện, không cần cơ chế đặc biệt, và hoàn toàn có thể bỏ qua nếu không phù hợp. Đó chính là tinh thần “minh bạch và kiểm soát” mà Zig theo đuổi.
Bài viết liên quan

Công nghệ
Mô hình AI hàng đầu giỏi Vật lý đến đâu? Nghiên cứu mới chỉ ra các bài kiểm tra hiện hành đang đánh giá sai
16 tháng 9, 2026

Công nghệ
Nộp đơn xin việc lẽ ra nên khó hơn. Thật đấy
25 tháng 8, 2026

Công nghệ
Vì sao các ngôn ngữ lập trình động dùng "shape" thay vì bảng băm để lưu thuộc tính?
22 tháng 10, 2026